322 lines
8.4 KiB
PHP
322 lines
8.4 KiB
PHP
<?php
|
|
|
|
namespace Phabel\PluginGraph;
|
|
|
|
use Phabel\PluginCache;
|
|
use Phabel\PluginInterface;
|
|
use SplObjectStorage;
|
|
use SplQueue;
|
|
|
|
/**
|
|
* Represents a plugin with a certain configuration.
|
|
*
|
|
* @author Daniil Gentili <daniil@daniil.it>
|
|
* @license MIT
|
|
*/
|
|
class Node
|
|
{
|
|
/**
|
|
* Plugins and configs.
|
|
*
|
|
* @var Plugins
|
|
*/
|
|
private Plugins $plugin;
|
|
|
|
/**
|
|
* Original plugin name.
|
|
*
|
|
* @var class-string<PluginInterface>
|
|
*/
|
|
private string $name;
|
|
|
|
/**
|
|
* Associated package context.
|
|
*
|
|
* @var PackageContext
|
|
*/
|
|
private PackageContext $packageContext;
|
|
/**
|
|
* Nodes that this node requires.
|
|
*
|
|
* @var SplObjectStorage<Node>
|
|
*/
|
|
private SplObjectStorage $requires;
|
|
|
|
/**
|
|
* Nodes that this node extends.
|
|
*
|
|
* @var SplObjectStorage<Node>
|
|
*/
|
|
private SplObjectStorage $extends;
|
|
|
|
/**
|
|
* Nodes that require this node.
|
|
*
|
|
* @var SplObjectStorage<Node>
|
|
*/
|
|
private SplObjectStorage $requiredBy;
|
|
|
|
/**
|
|
* Nodes that extend this node.
|
|
*
|
|
* @var SplObjectStorage<Node>
|
|
*/
|
|
private SplObjectStorage $extendedBy;
|
|
|
|
/**
|
|
* Graph instance.
|
|
*/
|
|
private GraphInternal $graph;
|
|
|
|
/**
|
|
* Whether this node was visited when looking for circular requirements.
|
|
*/
|
|
private bool $visitedCircular = false;
|
|
|
|
/**
|
|
* Whether this node can be required, or only extended.
|
|
*/
|
|
private bool $canBeRequired = true;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param GraphInternal $graph Graph instance
|
|
*/
|
|
public function __construct(GraphInternal $graph, PackageContext $ctx)
|
|
{
|
|
$this->packageContext = $ctx;
|
|
$this->graph = $graph;
|
|
$this->requiredBy = new SplObjectStorage;
|
|
$this->extendedBy = new SplObjectStorage;
|
|
$this->requires = new SplObjectStorage;
|
|
$this->extends = new SplObjectStorage;
|
|
}
|
|
/**
|
|
* Initialization function.
|
|
*
|
|
* @param string $plugin Plugin name
|
|
* @param array $config Plugin configuration
|
|
* @param PackageContext $ctx Context
|
|
*
|
|
* @psalm-param class-string<PluginInterface> $plugin Plugin name
|
|
*
|
|
* @return self
|
|
*/
|
|
public function init(string $plugin, array $config): self
|
|
{
|
|
$this->name = $plugin;
|
|
$this->plugin = new Plugins($plugin, $config);
|
|
|
|
$this->canBeRequired = PluginCache::canBeRequired($plugin);
|
|
foreach (PluginCache::runAfter($plugin, $config) as $class => $config) {
|
|
foreach ($this->graph->addPlugin($class, $config, $this->packageContext) as $node) {
|
|
$this->require($node);
|
|
}
|
|
}
|
|
foreach (PluginCache::runBefore($plugin, $config) as $class => $config) {
|
|
foreach ($this->graph->addPlugin($class, $config, $this->packageContext) as $node) {
|
|
$node->require($this);
|
|
}
|
|
}
|
|
foreach (PluginCache::runWithAfter($plugin, $config) as $class => $config) {
|
|
foreach ($this->graph->addPlugin($class, $config, $this->packageContext) as $node) {
|
|
$this->extend($node);
|
|
}
|
|
}
|
|
foreach (PluginCache::runWithBefore($plugin, $config) as $class => $config) {
|
|
foreach ($this->graph->addPlugin($class, $config, $this->packageContext) as $node) {
|
|
$node->extend($this);
|
|
}
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Make node require another node.
|
|
*
|
|
* @param self $node Node
|
|
*
|
|
* @return void
|
|
*/
|
|
private function require(self $node): void
|
|
{
|
|
if (!$node->canBeRequired) {
|
|
$this->extend($node);
|
|
return;
|
|
}
|
|
if ($this->extends->contains($node) || $node->extendedBy->contains($this)) {
|
|
$this->extends->detach($node);
|
|
$node->extendedBy->detach($this);
|
|
}
|
|
$this->requires->attach($node);
|
|
$node->requiredBy->attach($this);
|
|
|
|
$this->graph->linkNode($this);
|
|
}
|
|
/**
|
|
* Make node extend another node.
|
|
*
|
|
* @param self $node Node
|
|
*
|
|
* @return void
|
|
*/
|
|
private function extend(self $node): void
|
|
{
|
|
if ($this->requires->contains($node) || $node->requiredBy->contains($this)) {
|
|
return;
|
|
}
|
|
$this->extends->attach($node);
|
|
$node->extendedBy->attach($this);
|
|
|
|
$this->graph->linkNode($this);
|
|
}
|
|
|
|
/**
|
|
* Merge node with another node.
|
|
*
|
|
* @param self $other Other node
|
|
*
|
|
* @return Node
|
|
*/
|
|
public function merge(self $other): Node
|
|
{
|
|
$this->packageContext->merge($other->packageContext);
|
|
$this->plugin->merge($other->plugin);
|
|
foreach ($other->requiredBy as $node) {
|
|
$node->require($this);
|
|
$node->requires->detach($other);
|
|
}
|
|
foreach ($other->extendedBy as $node) {
|
|
$node->extend($this);
|
|
$node->extends->detach($other);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Flatten tree.
|
|
*
|
|
* @return SplQueue<SplQueue<PluginInterface>>
|
|
*/
|
|
public function flatten(): SplQueue
|
|
{
|
|
/** @var SplQueue<PluginInterface> */
|
|
$initQueue = new SplQueue;
|
|
|
|
/** @var SplQueue<SplQueue<PluginInterface>> */
|
|
$queue = new SplQueue;
|
|
$queue->enqueue($initQueue);
|
|
|
|
$this->flattenInternal($queue);
|
|
|
|
return $queue;
|
|
}
|
|
/**
|
|
* Look for circular references, while merging package contexts.
|
|
*
|
|
* @return self
|
|
*/
|
|
public function circular(): self
|
|
{
|
|
if ($this->visitedCircular) {
|
|
$plugins = [$this->name];
|
|
foreach (\debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, DEBUG_BACKTRACE_PROVIDE_OBJECT) as $frame) {
|
|
$plugins []= $frame['object']->name;
|
|
if ($frame['object'] === $this) {
|
|
break;
|
|
}
|
|
}
|
|
throw new CircularException($plugins);
|
|
}
|
|
$this->visitedCircular = true;
|
|
|
|
foreach ($this->requiredBy as $node) {
|
|
$this->packageContext->merge($node->circular()->packageContext);
|
|
}
|
|
foreach ($this->extendedBy as $node) {
|
|
$this->packageContext->merge($node->circular()->packageContext);
|
|
}
|
|
|
|
$this->visitedCircular = false;
|
|
|
|
return $this;
|
|
}
|
|
/**
|
|
* Internal flattening.
|
|
*
|
|
* @param SplQueue<SplQueue<PluginInterface>> $splQueue Queue
|
|
*
|
|
* @return void
|
|
*/
|
|
private function flattenInternal(SplQueue $splQueue): void
|
|
{
|
|
$queue = $splQueue->top();
|
|
$this->plugin->enqueue($queue, $this->packageContext);
|
|
|
|
/** @var SplQueue<Node> */
|
|
$extendedBy = new SplQueue;
|
|
$prevNode = null;
|
|
foreach ($this->extendedBy as $node) {
|
|
if (\count($node->requires) + \count($node->extends) === 1) {
|
|
if ($prevNode instanceof self) {
|
|
$node->merge($prevNode);
|
|
}
|
|
$prevNode = $node;
|
|
} else {
|
|
$extendedBy->enqueue($node);
|
|
}
|
|
}
|
|
if ($prevNode) {
|
|
$extendedBy->enqueue($prevNode);
|
|
}
|
|
|
|
/** @var SplQueue<Node> */
|
|
$requiredBy = new SplQueue;
|
|
$prevNode = null;
|
|
foreach ($this->requiredBy as $node) {
|
|
if (\count($node->requires) + \count($node->extends) === 1) {
|
|
if ($prevNode instanceof self) {
|
|
$node->merge($prevNode);
|
|
}
|
|
$prevNode = $node;
|
|
} else {
|
|
$requiredBy->enqueue($node);
|
|
}
|
|
}
|
|
if ($prevNode) {
|
|
$requiredBy->enqueue($prevNode);
|
|
}
|
|
|
|
|
|
|
|
foreach ($extendedBy as $node) {
|
|
$node->extends->detach($this);
|
|
if (\count($node->extends) + \count($node->requires) === 0) {
|
|
$node->flattenInternal($splQueue);
|
|
}
|
|
}
|
|
foreach ($requiredBy as $node) {
|
|
$node->requires->detach($this);
|
|
if (\count($node->extends) + \count($node->requires) === 0) {
|
|
$splQueue->enqueue(new SplQueue);
|
|
$node->flattenInternal($splQueue);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add packages from package context.
|
|
*
|
|
* @param PackageContext $ctx Package context
|
|
*
|
|
* @return void
|
|
*/
|
|
public function addPackages(PackageContext $ctx): void
|
|
{
|
|
$this->packageContext->merge($ctx);
|
|
}
|
|
}
|