Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mutable configurable #768

Merged
merged 2 commits into from May 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/Components/Blocks/Reference.php
Expand Up @@ -43,9 +43,7 @@ public static function build(
'title' => isset($matches[3]) ? $matches[3] : null,
];

$State = $State->setting(
$State->get(DefinitionBook::class)->setting($id, $Data)
);
$State->get(DefinitionBook::class)->mutatingSet($id, $Data);

return new self($State);
}
Expand Down
17 changes: 9 additions & 8 deletions src/Configurables/DefinitionBook.php
Expand Up @@ -2,12 +2,12 @@

namespace Erusev\Parsedown\Configurables;

use Erusev\Parsedown\Configurable;
use Erusev\Parsedown\MutableConfigurable;

/**
* @psalm-type _Data=array{url: string, title: string|null}
*/
final class DefinitionBook implements Configurable
final class DefinitionBook implements MutableConfigurable
{
/** @var array<string, _Data> */
private $book;
Expand All @@ -29,14 +29,10 @@ public static function initial()
/**
* @param string $id
* @param _Data $data
* @return self
*/
public function setting($id, array $data)
public function mutatingSet($id, array $data): void
{
$book = $this->book;
$book[$id] = $data;

return new self($book);
$this->book[$id] = $data;
}

/**
Expand All @@ -51,4 +47,9 @@ public function lookup($id)

return null;
}

public function isolatedCopy(): self
{
return new self($this->book);
}
}
44 changes: 44 additions & 0 deletions src/MutableConfigurable.php
@@ -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();
}
4 changes: 2 additions & 2 deletions src/Parsedown.php
Expand Up @@ -31,7 +31,7 @@ public function __construct(StateBearer $StateBearer = null)
{
$StateBearer = $StateBearer ?: new State;

$this->State = $StateBearer->state();
$this->State = $StateBearer->state()->isolatedCopy();
}

/**
Expand All @@ -42,7 +42,7 @@ public function toHtml($markdown)
{
list($StateRenderables, $State) = self::lines(
Lines::fromTextLines($markdown, 0),
$this->State
$this->State->isolatedCopy()
);

$Renderables = $State->applyTo($StateRenderables);
Expand Down
28 changes: 27 additions & 1 deletion src/State.php
Expand Up @@ -15,7 +15,7 @@ final class State implements StateBearer
/**
* @var array<class-string<Configurable>, Configurable>
*/
private static $initialCache;
private static $initialCache = [];

/**
* @param Configurable[] $Configurables
Expand Down Expand Up @@ -56,6 +56,22 @@ public function mergingWith(State $State)
*/
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 */
return (
$this->state[$className]
Expand Down Expand Up @@ -93,4 +109,14 @@ public function state()
{
return $this;
}

public function isolatedCopy(): self
{
return new self(\array_map(
function ($C) {
return $C instanceof MutableConfigurable ? $C->isolatedCopy() : $C;
},
$this->state
));
}
}