Add MutableConfigurable

Hopefully I do not regret this...
This commit is contained in:
Aidan Woods 2020-05-05 19:09:17 +01:00
parent 8e8d1dac21
commit 0ef406e6d2
No known key found for this signature in database
GPG Key ID: 9A6A8EFAA512BBB9
3 changed files with 73 additions and 3 deletions

View File

@ -0,0 +1,44 @@
<?php
namespace Erusev\Parsedown;
/**
* Beware that the values of MutableConfigurables are NOT stable. Values SHOULD
* be accessed as close to use as possible. Parsing operations sharing the same
* State SHOULD NOT be triggered between where values are read and where they
* need to be relied upon.
*/
interface MutableConfigurable extends Configurable
{
/**
* Objects contained in State can generally be regarded as immutable,
* however, when mutability is *required* then isolatedCopy (this method)
* MUST be implemented to take a reliable copy of the contained state,
* which MUST be fully seperable from the current instance. This is
* sometimes referred to as a "deep copy".
*
* The following assumption is made when you implement
* MutableConfigurable:
*
* A shared, (more or less) globally writable, instantaniously updating
* (at all parsing levels), single copy of a Configurable is intentional
* and desired.
*
* As such, Parsedown will use the isolatedCopy method to ensure state
* isolation between successive parsing calls (which are considered to be
* isolated documents).
*
* You MUST NOT depend on the method `initial` being called when a clean
* parsing state is desired, this will not reliably occur; implement
* isolatedCopy properly to allow Parsedown to manage this.
*
* Failing to implement this method properly can result in unintended
* side-effects. If possible, you should design your Configurable to be
* immutable, which allows a single copy to be shared safely, and mutations
* localised to a heirarchy for which the order of operations is easy to
* reason about.
*
* @return static
*/
public function isolatedCopy();
}

View File

@ -31,7 +31,7 @@ final class Parsedown
{ {
$StateBearer = $StateBearer ?: new State; $StateBearer = $StateBearer ?: new State;
$this->State = $StateBearer->state(); $this->State = $StateBearer->state()->isolatedCopy();
} }
/** /**
@ -42,7 +42,7 @@ final class Parsedown
{ {
list($StateRenderables, $State) = self::lines( list($StateRenderables, $State) = self::lines(
Lines::fromTextLines($markdown, 0), Lines::fromTextLines($markdown, 0),
$this->State $this->State->isolatedCopy()
); );
$Renderables = $State->applyTo($StateRenderables); $Renderables = $State->applyTo($StateRenderables);

View File

@ -15,7 +15,7 @@ final class State implements StateBearer
/** /**
* @var array<class-string<Configurable>, Configurable> * @var array<class-string<Configurable>, Configurable>
*/ */
private static $initialCache; private static $initialCache = [];
/** /**
* @param Configurable[] $Configurables * @param Configurable[] $Configurables
@ -56,6 +56,22 @@ final class State implements StateBearer
*/ */
public function get($className) public function get($className)
{ {
if (
! isset($this->state[$className])
&& \is_subclass_of($className, MutableConfigurable::class, true)
) {
if (! isset(self::$initialCache[$className])) {
/** @var T */
self::$initialCache[$className] = $className::initial();
}
/**
* @var T
* @psalm-suppress PossiblyUndefinedMethod
*/
$this->state[$className] = self::$initialCache[$className]->isolatedCopy();
}
/** @var T */ /** @var T */
return ( return (
$this->state[$className] $this->state[$className]
@ -93,4 +109,14 @@ final class State implements StateBearer
{ {
return $this; return $this;
} }
public function isolatedCopy(): self
{
return new self(\array_map(
function ($C) {
return $C instanceof MutableConfigurable ? $C->isolatedCopy() : $C;
},
$this->state
));
}
} }