Actualización

This commit is contained in:
Xes
2025-04-10 12:24:57 +02:00
parent 8969cc929d
commit 45420b6f0d
39760 changed files with 4303286 additions and 0 deletions

2
vendor/clue/graph/.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
github: clue
custom: https://clue.engineering/support

376
vendor/clue/graph/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,376 @@
# Changelog
## 0.9.3 (2021-12-30)
* Feature: Support PHP 8.1 release.
(#208 by @clue)
* Fix: Fix automatic vertex ID generation when using vertex IDs with strings.
(#204 by @viktorprogger)
* Improve test suite and use GitHub Actions for continuous integration (CI).
(#207 by @clue)
## 0.9.2 (2020-12-03)
* Feature: Support PHP 8 and PHPUnit 9.3.
(#200 by @SimonFrings)
## 0.9.1 (2019-10-02)
* Fix: Deleting vertex with loop edge no longer fails.
(#149 by @tomzx)
* Fix: Fix returning directed loop edges and adjacent vertices from vertex twice.
(#170 by @clue)
* Minor documentation updates and fixes.
(#153 by @marclaporte and #163, #164 and #172 by @clue)
* Improve test suite to move tests to `Fhaculty\Graph\Tests` namespace,
update test suite to support PHPUnit 6 and PHPUnit 5 and
support running on legacy PHP 5.3 through PHP 7.2 and HHVM.
(#148 by @tomzx and #150 and #162 by @clue)
* Originally planned to add a new `AttributeAware::removeAttribute()` method,
but reverted due to BC break. Change will be reconsidered for next major release.
(#138 and #171 by @johnathanmdell and @clue)
## 0.9.0 (2015-03-07)
* BC break: Split off individual components in order to stabilize core graph lib.
([#120](https://github.com/clue/graph/issues/120))
* Split off `Algorithm` namespace into separate [graphp/algorithms](https://github.com/graphp/algorithms) package.
([#119](https://github.com/clue/graph/issues/119))
* Split off `Exporter\TrivialGraphFormat` into separate [graphp/trivial-graph-format](https://github.com/graphp/trivial-graph-format) package.
([#121](https://github.com/clue/graph/issues/121))
* Split off `Loader` namespace into separate [graphp/plaintext](https://github.com/graphp/plaintext) package.
([#117](https://github.com/clue/graph/issues/117))
* BC break: Remove Exporter from `Graph` and `Graph::__toString()` (trivial graph format exporter has been split off).
([#122](https://github.com/clue/graph/pull/122))
* BC break: Vertices can no longer be sorted by (in/out)degree (degree algorithm has been split off).
([#128](https://github.com/clue/graph/pull/128))
* Apply PSR-4 layout under `src/` and add tests to achieve 100% test coverage.
([#127](https://github.com/clue/graph/issues/127) & [#129](https://github.com/clue/graph/issues/129))
## 0.8.0 (2014-12-31)
* Feature: Add general purpose Attributes.
([#103](https://github.com/clue/graph/pull/103))
* BC break: Split off all GraphViz-related classes to a separate
[graphp/graphviz](https://github.com/graphp/graphviz) package.
([#115](https://github.com/clue/graph/pull/115))
* Feature: The base `Graph`, `Vertex` and `EdgeBase` classes can now be
extended in order to implement a custom behavior. As such, one can now also
instantiate them using the normal `new` operator instead of having to use
`Graph::createVertex()` family of methods.
([#82](https://github.com/clue/graph/issues/82))
* BC break: Rename `Algorithm\Directed::isDirected()` to remove its ambiguity
in regards to mixed and/or empty graphs
([#80](https://github.com/clue/graph/issues/80))
| Old name | New name |
|---|---|
| `Algorithm\Directed::isDirected()` | `Algorithm\Directed::hasDirected()` |
* Feature: Add new `Algorithm\Directed::hasUndirected()` and
`Algorithm\Directed::isMixed()` in order to complement the renamed
`Algorithm\Directed::hasDirected()`
([#80](https://github.com/clue/graph/issues/80))
* BC break: `Walk::factoryCycleFromVertices()` no longer tries to auto-complete
a cycle if the first vertex does not match the last one, but now throws an
`InvalidArgumentException` instead ([#87](https://github.com/clue/graph/issues/87))
* Feature: Support loop `Walk`s, i.e. a walk with only a single edge from
vertex A back to A ([#87](https://github.com/clue/graph/issues/87))
* Fix: Stricter checks for invalid cycles, such as one with an invalid
predecessor-map or no edges at all ([#87](https://github.com/clue/graph/issues/87))
* Fix: The `Algorithm\ShortestPath\MooreBellmanFord` now also works for unweighted
edges. This also fixes an issue where `Algorithm\DetectNegativeCycle` didn't work
for unweighted edges. ([#81](https://github.com/clue/graph/issues/81))
* Fix: The `Algorithm\MinimumCostFlow` algorithms now work again. The reference
to a non-existant class has been updated. Also fixed several issues with
regards to special cases such as disconnected or undirected graphs.
([#74](https://github.com/clue/graph/issues/74))
* BC break: Remove unneeded alias definitions of `getVertexFirst()`,
`getVertexSource()` and `getVertexTarget()`
([#76](https://github.com/clue/graph/issues/76)):
| Old name | New name |
|---|---|
| `Graph::getVertexFirst()` | `Graph::getVertices()->getVertexFirst()` |
| `Walk::getVertexSource()` | `Walk::getVertices()->getVertexFirst()` |
| `Walk::getVertexTarget()` | `Walk::getVertices()->getVertexLast()` |
## 0.7.1 (2014-03-12)
* Fix: Throwing an `UnexpectedValueException` if writing GraphViz Dot script
to a temporary file fails and remove its debugging output
([#77](https://github.com/clue/graph/issues/77) and [#78](https://github.com/clue/graph/issues/78) @Metabor)
* Fix: Improved GraphViz support for MS Windows
([#99](https://github.com/clue/graph/issues/99))
## 0.7.0 (2013-09-11)
* Feature: Add new `Set\Vertices` and `Set\Edges` classes that handle common
operations on a Set of multiple `Vertex` and `Edge` instances respectively.
([#48](https://github.com/clue/graph/issues/48))
* BC break: Move operations and their corresponding constants concerning Sets
to their corresponding Sets:
| Old name | New name |
|---|---|
| `Edge\Base::getFirst()` | `Set\Edges::getEdgeOrder()` |
| `Edge\Base::getAll()` | `Set\Edges::getEdgesOrder()` |
| `Edge\Base::ORDER_*` | `Set\Edges::ORDER_*` |
|---|---|
| `Vertex::getFirst()` | `Set\Vertices::getVertexOrder()` |
| `Vertex::getAll()` | `Set\Vertices::getVerticesOrder()` |
| `Vertex::ORDER_` | `Set\Vertices::ORDER_*` |
* BC break: Each `getVertices*()` and `getEdges*()` method now returns a `Set`
instead of a primitive array of instances. *Most* of the time this should
work without changing your code, because each `Set` implements an `Iterator`
interface and can easily be iterated using `foreach`. However, using a `Set`
instead of a plain array differs when checking its boolean value or
comparing two Sets. I.e. if you happen to want to check if an `Set` is empty,
you now have to use the more explicit syntax `$set->isEmpty()`.
* BC break: `Vertex::getVertices()`, `Vertex::getVerticesEdgeTo()` and
`Vertex::getVerticesEdgeFrom()` now return a `Set\Vertices` instance that
may contain duplicate vertices if parallel (multiple) edges exist. Previously
there was no easy way to detect this situation - this is now the default. If
you also want to get unique / distinct `Vertex` instances, use
`Vertex::getVertices()->getVerticesDistinct()` where applicable.
* BC break: Remove all occurances of `getVerticesId()`, use
`getVertices()->getIds()` instead.
* BC break: Merge `Cycle` into `Walk` ([#61](https://github.com/clue/graph/issues/61)).
As such, its static factory methods had to be renamed. Update your references if applicable:
| Old name | New name |
|---|---|
| `Cycle::factoryFromPredecessorMap()` | `Walk::factoryCycleFromPredecessorMap()` |
| `Cycle::factoryFromVertices()` | `Walk::factoryCycleFromVertices()` |
| `Cycle::factoryFromEdges()` | `Walk::factoryCycleFromEdges()` |
* BC break: Remove `Graph::isEmpty()` because it's not well-defined and might
be confusing. Most literature suggests it should check for existing edges,
whereas the old behavior was to check for existing vertices instead. Use either
of the new and more transparent methods
`Algorithm\Property\GraphProperty::isNull()` (old behavior) or (where applicable)
`Algorithm\Property\GraphProperty::isEdgeless()` ([#63](https://github.com/clue/graph/issues/63)).
* BC break: Each of the above methods (`Walk::factoryCycleFromPredecessorMap()`,
`Walk::factoryCycleFromVertices()`, `Walk::factoryCycleFromEdges()`) now
actually makes sure the returned `Walk` instance is actually a valid Cycle,
i.e. the start `Vertex` is the same as the end `Vertex` ([#61](https://github.com/clue/graph/issues/61))
* BC break: Each `Algorithm\ShortestPath` algorithm now consistenly does not
return a zero weight for the root Vertex and now supports loop edges on the root
Vertex ([#62](https://github.com/clue/graph/issues/62))
* BC break: Each `Algorithm\ShortestPath` algorithm now consistently throws an
`OutOfBoundsException` for unreachable vertices
([#62](https://github.com/clue/graph/issues/62))
* BC break: A null Graph (a Graph with no Vertices and thus no Edges) is not a
valid tree (because it is not connected), adjust `Algorithm\Tree\Base::isTree()`
accordingly.
([#72](https://github.com/clue/graph/issues/72))
* BC break: Remove all occurances of `getNumberOfVertices()` and
`getNumberOfEdges()` ([#75](https://github.com/clue/graph/issues/75) and
[#48](https://github.com/clue/graph/issues/48)):
| Old name | New name |
|---|---|
| `$set->getNumberOfVertices()` | `count($set->getVertices())` |
| `$set->getNumberOfEdges()` | `count($set->getEdges())` |
* BC break: Replace base `Set` class with `Set\DualAggregate` interface. This
is unlikely to affect you, but might potentially break your custom
inheritance or polymorphism for algorithms.
([#75](https://github.com/clue/graph/issues/75))
* Feature: Add `Algorithm\ShortestPath\Base::hasVertex(Vertex $vertex)` to check whether
a path to the given Vertex exists ([#62](https://github.com/clue/graph/issues/62)).
* Feature: Support opening GraphViz images on Mac OS X in default image viewer
([#67](https://github.com/clue/graph/issues/67) @onigoetz)
* Feature: Add `Algorithm\MinimumSpanningTree\Base::getWeight()` to get total
weight of resulting minimum spanning tree (MST).
([#73](https://github.com/clue/graph/issues/73))
* Feature: Each `Algorithm\MinimumSpanningTree` algorithm now supports
undirected and mixed Graphs, as well as null weights for Edges.
([#73](https://github.com/clue/graph/issues/73))
* BC break: Each `Algorithm\MinimumSpanningTree` algorithm now throws an
`UnexpectedValueException` for unconnected Graphs (and thus also null Graphs).
([#73](https://github.com/clue/graph/issues/73))
* Feature: Add `Walk::factoryFromVertices()`
([#64](https://github.com/clue/graph/issues/64)).
* Fix: Checking `Walk::isValid()`
([#61](https://github.com/clue/graph/issues/61))
* Fix: Missing import prevented
`Algorithm\ShortestPath\MooreBellmanFord::getCycleNegative()` from actually
throwing the right `UnderflowException` if no cycle was found
([#62](https://github.com/clue/graph/issues/62))
* Fix: Calling `Exporter\Image::setFormat()` had no effect due to misassignment
([#70](https://github.com/clue/graph/issues/70) @FGM)
## 0.6.0 (2013-07-11)
* BC break: Move algorithm definitions in base classes to separate algorithm classes ([#27](https://github.com/clue/graph/issues/27)).
The following methods containing algorithms were now moved to separate algorithm classes. This
change encourages code-reuse, simplifies spotting algorithms, helps reducing complexity,
improves testablity and avoids tight coupling. Update your references if applicable:
| Old name | New name | Related ticket |
|---|---|---|
| `Set::getWeight()` | `Algorithm\Weight::getWeight()` | [#33](https://github.com/clue/graph/issues/33) |
| `Set::getWeightFlow()` | `Algorithm\Weight::getWeightFlow()` | [#33](https://github.com/clue/graph/issues/33) |
| `Set::getWeightMin()` | `Algorithm\Weight::getWeightMin()` | [#33](https://github.com/clue/graph/issues/33) |
| `Set::isWeighted()` | `Algorithm\Weight::isWeighted()` | [#33](https://github.com/clue/graph/issues/33) |
|-|-|-|
| `Graph::getDegree()` | `Algorithm\Degree::getDegree()` | [#29](https://github.com/clue/graph/issues/29) |
| `Graph::getDegreeMin()` | `Algorithm\Degree::getDegreeMin()` | [#29](https://github.com/clue/graph/issues/29) |
| `Graph::getDegreeMax()` | `Algorithm\Degree::getDegreeMax()` | [#29](https://github.com/clue/graph/issues/29) |
| `Graph::isRegular()` | `Algorithm\Degree::isRegular()` | [#29](https://github.com/clue/graph/issues/29) |
| `Graph::isBalanced()` | `Algorithm\Degree::isBalanced()` | [#29](https://github.com/clue/graph/issues/29) |
| `Vertex::getDegree()` | `Algorithm\Degree:getDegreeVertex()` | [#49](https://github.com/clue/graph/issues/49) |
| `Vertex::getDegreeIn()` | `Algorithm\Degree:getDegreeInVertex()` | [#49](https://github.com/clue/graph/issues/49) |
| `Vertex::getDegreeOut()` | `Algorithm\Degree:getDegreeOutVertex()` | [#49](https://github.com/clue/graph/issues/49) |
| `Vertex::isSink()` | `Algorithm\Degree:isVertexSink()` | [#49](https://github.com/clue/graph/issues/49) |
| `Vertex::isSource()` | `Algorithm\Degree:isVertexSource()` | [#49](https://github.com/clue/graph/issues/49) |
| `Vertex::isIsolated()` | `Algorithm\Degree::isVertexIsolated()` | [#49](https://github.com/clue/graph/issues/49) |
|-|-|-|
| `Set::isDirected()` | `Algorithm\Directed::isDirected()` | [#34](https://github.com/clue/graph/issues/34) |
|-|-|-|
| `Graph::isSymmetric()` | `Algorithm\Symmetric::isSymmetric()` | [#41](https://github.com/clue/graph/issues/41) |
|-|-|-|
| `Graph::isComplete()` | `Algorithm\Complete::isComplete()` | [#43](https://github.com/clue/graph/issues/43) |
|-|-|-|
| `Set::hasFlow()` | `Algorithm\Flow::hasFlow()` | [#47](https://github.com/clue/graph/issues/47) |
| `Graph::getBalance()` | `Algorithm\Flow::getBalance()` | [#30](https://github.com/clue/graph/issues/30), [#47](https://github.com/clue/graph/issues/47) |
| `Graph::isBalancedFlow()` | `Algorithm\Flow::isBalancedFlow()` | [#30](https://github.com/clue/graph/issues/39), [#47](https://github.com/clue/graph/issues/47) |
| `Vertex::getFlow()` | `Algorithm\Flow::getFlowVertex()` | [#47](https://github.com/clue/graph/issues/47) |
|-|-|-|
| `Vertex::isLeaf()` | `Algorithm\Tree\Undirected::isVertexLeaf()` | [#44](https://github.com/clue/graph/issues/44) |
|-|-|-|
| `Set::hasLoop()` | `Algorithm\Loop::hasLoop()` | [#51](https://github.com/clue/graph/issues/51) |
| `Vertex::hasLoop()` | `Algorithm\Loop::hasLoopVertex()` | [#51](https://github.com/clue/graph/issues/51) |
|-|-|-|
| `Set::hasEdgeParallel()` | `Algorithm\Parallel::hasEdgeParallel()` | [#52](https://github.com/clue/graph/issues/52) |
| `Edge\Base::hasEdgeParallel()` | `Algorithm\Parallel::hasEdgeParallelEdge()` | [#52](https://github.com/clue/graph/issues/52) |
| `Edge\Base::getEdgesParallel()` | `Algorithm\Parallel::getEdgeParallelEdge()` | [#52](https://github.com/clue/graph/issues/52) |
|-|-|-|
| `Graph::isEdgeless()` | `Algorithm\Property\GraphProperty::isEdgeless()` | [#54](https://github.com/clue/graph/issues/54) |
| `Graph::isTrivial()` | `Algorithm\Property\GraphProperty::isTrivial()` | [#54](https://github.com/clue/graph/issues/54) |
| `Walk::isCycle()` | `Algorithm\Property\WalkProperty::isCycle()` | [#54](https://github.com/clue/graph/issues/54) |
| `Walk::isPath()` | `Algorithm\Property\WalkProperty::isPath()` | [#54](https://github.com/clue/graph/issues/54) |
| `Walk::hasCycle()` | `Algorithm\Property\WalkProperty::hasCycle()` | [#54](https://github.com/clue/graph/issues/54) |
| `Walk::isLoop()` | `Algorithm\Property\WalkProperty::isLoop()` | [#54](https://github.com/clue/graph/issues/54) |
| `Walk::isDigon()` | `Algorithm\Property\WalkProperty::isDigon()` | [#54](https://github.com/clue/graph/issues/54) |
| `Walk::isTriangle()` | `Algorithm\Property\WalkProperty::isTriangle()` | [#54](https://github.com/clue/graph/issues/54) |
| `Walk::isSimple()` | `Algorithm\Property\WalkProperty::isSimple()` | [#54](https://github.com/clue/graph/issues/54) |
| `Walk::isHamiltonian()` | `Algorithm\Property\WalkProperty::isHamiltonian()` | [#54](https://github.com/clue/graph/issues/54) |
| `Walk::isEulerian()` | `Algorithm\Property\WalkProperty::isEulerian()` | [#54](https://github.com/clue/graph/issues/54) |
* BC break: Remove unneeded algorithm alias definitions ([#31](https://github.com/clue/graph/issues/31), [#50](https://github.com/clue/graph/issues/50)). The following *alias definitions*
have been removed, their original/actual name has already existed before and continues to work
unchanged. Update your references if applicable:
| Old/removed alias definition | Actual name |
|---|---|
| `Graph::isConnected()` | `Algorithm\ConnectedComponents::isSingle()` |
| `Graph::hasEulerianCycle()` | `Algorithm\Eulerian::hasCycle()` |
| `Graph::getNumberOfComponents()` | `Algorithm\ConnectedComponents::getNumberOfComponents()` |
| `Graph::getNumberOfGroups()` | `Algorithm\Groups::getNumberOfGroups()` |
| `Graph::isBipartit()` | `Algorithm\Bipartit::isBipartit()` |
| `Vertex::hasPathTo()` | `Algorithm\ShortestPath\BreadthFirst::hasVertex()` |
| `Vertex::hasPathFrom()` | `Algorithm\ShortestPath\BreadthFirst::hasVertex()` |
| `Vertex::getVerticesPathTo()` | `Algorithm\ShortestPath\BreadthFirst::getVertices()` |
| `Vertex::getVerticesPathFrom()` | `Algorithm\ShortestPath\BreadthFirst::getVertices()` |
* BC break: `Graph::createVertices()` now returns an array of vertices instead of the
chainable `Graph` ([#19](https://github.com/clue/graph/issues/19))
* BC break: Move `Loader\UmlClassDiagram` to separate [fhaculty/graph-uml](https://github.com/fhaculty/graph-uml)
repo ([#38](https://github.com/clue/graph/issues/38))
* BC break: Remove needless `Algorithm\MinimumSpanningTree\PrimWithIf`
(use `Algorithm\MinimumSpanningTree\Prim` instead)
([#45](https://github.com/clue/graph/issues/45))
* BC break: `Vertex::createEdgeTo()` now returns an instance of type
`Edge\Undirected` instead of `Edge\UndirectedId`
([#46](https://github.com/clue/graph/issues/46))
* BC break: `Edge\Base::setCapacity()` now consistently throws an `RangeException`
instead of `InvalidArgumentException` if the current flow exceeds the new maximum
capacity ([#53](https://github.com/clue/graph/issues/53))
* Feature: New `Algorithm\Tree` namespace with algorithms for undirected and directed,
rooted trees ([#44](https://github.com/clue/graph/issues/44))
* Feature: According to be above list of moved algorithm methods, the following algorithm
classes have been added ([#27](https://github.com/clue/graph/issues/27)):
* New `Algorithm\Weight` ([#33](https://github.com/clue/graph/issues/33))
* New `Algorithm\Degree` ([#29](https://github.com/clue/graph/issues/29), [#49](https://github.com/clue/graph/issues/49))
* New `Algorithm\Directed` ([#34](https://github.com/clue/graph/issues/34))
* New `Algorithm\Symmetric` ([#41](https://github.com/clue/graph/issues/41))
* New `Algorithm\Complete` ([#43](https://github.com/clue/graph/issues/43))
* New `Algorithm\Flow` ([#30](https://github.com/clue/graph/issues/30), [#47](https://github.com/clue/graph/issues/47))
* New `Algorithm\Tree` ([#44](https://github.com/clue/graph/issues/44))
* New `Algorithm\Loop` ([#51](https://github.com/clue/graph/issues/51))
* New `Algorithm\Parallel` ([#52](https://github.com/clue/graph/issues/52))
* New `Algorithm\Property` ([#54](https://github.com/clue/graph/issues/54))
* Feature: `Graph::createVertices()` now also accepts an array of vertex IDs
([#19](https://github.com/clue/graph/issues/19))
* Feature: Add `Algorithm\Property\WalkProperty::hasLoop()` alias definition for
completeness ([#54](https://github.com/clue/graph/issues/54))
* Feature: Add `Algorithm\Property\WalkProperty::isCircuit()` definition to distinguish
circuits from cycles ([#54](https://github.com/clue/graph/issues/54))
* Fix: Checking hamiltonian cycles always returned false
([#54](https://github.com/clue/graph/issues/54))
* Fix: A Walk with no edges is no longer considered a valid cycle
([#54](https://github.com/clue/graph/issues/54))
* Fix: Various issues with `Vertex`/`Edge` layout attributes
([#32](https://github.com/clue/graph/issues/32))
* Fix: Getting multiple parallel edges for undirected edges
([#52](https://github.com/clue/graph/issues/52))
## 0.5.0 (2013-05-07)
* First tagged release (See issue [#20](https://github.com/clue/graph/issues/20) for more info on why it starts as v0.5.0)

22
vendor/clue/graph/LICENSE vendored Normal file
View File

@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2012+ Christian Lück (Maintainer)
Copyright (c) 2012+ Fhaculty Core Team and our awesome contributors <https://github.com/clue/graph/graphs/contributors>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

160
vendor/clue/graph/README.md vendored Normal file
View File

@@ -0,0 +1,160 @@
# graphp/graph
[![CI status](https://github.com/graphp/graph/actions/workflows/ci.yml/badge.svg?branch=v0.9.x)](https://github.com/graphp/graph/actions)
GraPHP is the mathematical graph/network library written in PHP.
> You're viewing the contents of the `v0.9.x` release branch, note that active
development continues on another branch, see `master` branch for more details.
**Table of contents**
* [Quickstart examples](#quickstart-examples)
* [Features](#features)
* [Components](#components)
* [Graph drawing](#graph-drawing)
* [Common algorithms](#common-algorithms)
* [Install](#install)
* [Tests](#tests)
* [Contributing](#contributing)
* [License](#license)
## Quickstart examples
Once [installed](#install), let's initialize a sample graph:
```php
<?php
require __DIR__ . '/vendor/autoload.php';
$graph = new Fhaculty\Graph\Graph();
// create some cities
$rome = $graph->createVertex('Rome');
$madrid = $graph->createVertex('Madrid');
$cologne = $graph->createVertex('Cologne');
// build some roads
$cologne->createEdgeTo($madrid);
$madrid->createEdgeTo($rome);
// create loop
$rome->createEdgeTo($rome);
```
Let's see which city (Vertex) has a road (i.e. an edge pointing) to Rome:
```php
foreach ($rome->getVerticesEdgeFrom() as $vertex) {
echo $vertex->getId().' leads to rome'.PHP_EOL;
// result: Madrid and Rome itself
}
```
## Features
This library is built around the concept of [mathematical graph theory](https://en.wikipedia.org/wiki/Graph_theory) (i.e. it is **not** a [charting](https://en.wikipedia.org/wiki/Chart) library for drawing a [graph of a function](https://en.wikipedia.org/wiki/Graph_of_a_function)). In essence, a graph is a set of *nodes* with any number of *connections* in between. In graph theory, [vertices](https://en.wikipedia.org/wiki/Vertex_%28graph_theory%29) (plural of vertex) are an abstract representation of these *nodes*, while *connections* are represented as *edges*. Edges may be either undirected ("two-way") or directed ("one-way", aka di-edges, arcs).
Depending on how the edges are constructed, the whole graph can either be undirected, can be a [directed graph](https://en.wikipedia.org/wiki/Directed_graph) (aka digraph) or be a [mixed graph](https://en.wikipedia.org/wiki/Mixed_graph). Edges are also allowed to form [loops](https://en.wikipedia.org/wiki/Loop_%28graph_theory%29) (i.e. an edge from vertex A pointing to vertex A again). Also, [multiple edges](https://en.wikipedia.org/wiki/Multiple_edges) from vertex A to vertex B are supported as well (aka parallel edges), effectively forming a [multigraph](https://en.wikipedia.org/wiki/Multigraph) (aka pseudograph). And of course, any combination thereof is supported as well. While many authors try to differentiate between these core concepts, this library tries hard to not impose any artificial limitations or assumptions on your graphs.
## Components
This library provides the core data structures for working with graphs, its vertices, edges and attributes.
There are several official components built on top of these structures to provide commonly needed functionality.
This architecture allows these components to be used independently and on demand only.
Following is a list of some highlighted components. A list of all official components can be found in the [graphp project](https://github.com/graphp).
### Graph drawing
This library is built to support visualizing graph images, including them into webpages, opening up images from within CLI applications and exporting them as PNG, JPEG or SVG file formats (among many others). Because [graph drawing](https://en.wikipedia.org/wiki/Graph_drawing) is a complex area on its own, the actual layouting of the graph is left up to the excellent [GraphViz](https://www.graphviz.org/) "Graph Visualization Software" and we merely provide some convenient APIs to interface with GraphViz.
See [graphp/graphviz](https://github.com/graphp/graphviz) for more details.
### Common algorithms
Besides graph drawing, one of the most common things to do with graphs is running algorithms to solve common graph problems.
Therefore this library is being used as the basis for implementations for a number of commonly used graph algorithms:
* Search
* Deep first (DFS)
* Breadth first search (BFS)
* Shortest path
* [Dijkstra](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm)
* Moore-Bellman-Ford (MBF)
* Counting number of hops (simple BFS)
* [Minimum spanning tree (MST)](https://en.wikipedia.org/wiki/Minimum_spanning_tree)
* Kruskal
* Prim
* [Traveling salesman problem (TSP)](https://en.wikipedia.org/wiki/Travelling_salesman_problem)
* Bruteforce algorithm
* Minimum spanning tree heuristic (TSP MST heuristic)
* Nearest neighbor heuristic (NN heuristic)
* Maximum flow
* [Edmonds-Karp](https://en.wikipedia.org/wiki/Edmonds%E2%80%93Karp_algorithm)
* Minimum cost flow (MCF)
* Cycle canceling
* Successive shortest path
* Maximum matching
* Flow algorithm
See [graphp/algorithms](https://github.com/graphp/algorithms) for more details.
## Install
The recommended way to install this library is [through Composer](https://getcomposer.org/).
[New to Composer?](https://getcomposer.org/doc/00-intro.md)
This will install the latest supported version:
```bash
$ composer require clue/graph:^0.9.3
```
See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
This project aims to run on any platform and thus does not require any PHP
extensions and supports running on legacy PHP 5.3 through current PHP 8+ and
HHVM.
It's *highly recommended to use the latest supported PHP version* for this project.
You may also want to install some of the [additional components](#components).
A list of all official components can be found in the [graphp project](https://github.com/graphp).
## Tests
This library uses PHPUnit for its extensive test suite.
To run the test suite, you first need to clone this repo and then install all
dependencies [through Composer](https://getcomposer.org/):
```bash
$ composer install
```
To run the test suite, go to the project root and run:
```bash
$ vendor/bin/phpunit
```
## Contributing
This library comes with an extensive test suite and is regularly tested and used in the *real world*.
Despite this, this library is still considered beta software and its API is subject to change.
The [changelog](CHANGELOG.md) lists all relevant information for updates between releases.
If you encounter any issues, please don't hesitate to drop us a line, file a bug report or even best provide us with a patch / pull request and/or unit test to reproduce your problem.
Besides directly working with the code, any additional documentation, additions to our readme or even fixing simple typos are appreciated just as well.
Any feedback and/or contribution is welcome!
Check out #graphp on irc.freenode.net.
## License
This project is released under the permissive [MIT license](LICENSE).
> Did you know that I offer custom development services and issuing invoices for
sponsorships of releases and for contributions? Contact me (@clue) for details.

40
vendor/clue/graph/composer.json vendored Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "clue/graph",
"type": "library",
"description": "GraPHP is the mathematical graph/network library written in PHP.",
"keywords": [
"graph",
"network",
"mathematical",
"vertex",
"edge"
],
"homepage": "https://github.com/graphp/graph",
"license": "MIT",
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering"
}
],
"autoload": {
"psr-4": {
"Fhaculty\\Graph\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Fhaculty\\Graph\\Tests\\": "tests/"
}
},
"require": {
"php": ">=5.3"
},
"require-dev": {
"phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35"
},
"suggest": {
"graphp/graphviz": "GraphViz graph drawing / DOT output",
"graphp/algorithms": "Common graph algorithms, such as Dijkstra and Moore-Bellman-Ford (shortest path), minimum spanning tree (MST), Kruskal, Prim and many more.."
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Fhaculty\Graph\Attribute;
/**
* Implemented by any entity that is aware of additional attributes
*
* Each attribute consists of a name (string) and an arbitrary value.
*/
interface AttributeAware
{
/**
* get a single attribute with the given $name (or return $default if attribute was not found)
*
* @param string $name
* @param mixed $default to return if attribute was not found
* @return mixed
*/
public function getAttribute($name, $default = null);
/**
* set a single attribute with the given $name to given $value
*
* @param string $name
* @param mixed $value
*/
public function setAttribute($name, $value);
/**
* get a container for all attributes
*
* @return AttributeBag
*/
public function getAttributeBag();
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Fhaculty\Graph\Attribute;
/**
* Interface to container that represents multiple attributes
*/
interface AttributeBag extends AttributeAware
{
// public function getAttribute($name, $default);
// public function setAttribute($name, $value);
// public function getAttributeBag();
/**
* set an array of additional attributes
*
* @param array $attributes
*/
public function setAttributes(array $attributes);
/**
* get an array of all attributes
*
* @return array
*/
public function getAttributes();
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Fhaculty\Graph\Attribute;
/**
* A fairly standard AttributeBag container.
*
* This container passes and returns attributes by value. It is mutable,
* however, so multiple references to the container will update in kind.
*/
class AttributeBagContainer implements AttributeBag
{
/**
* @var array
*/
private $attributes = array();
/**
* get a single attribute with the given $name (or return $default if attribute was not found)
*
* @param string $name
* @param mixed $default to return if attribute was not found
* @return mixed
*/
public function getAttribute($name, $default = null)
{
return isset($this->attributes[$name]) ? $this->attributes[$name] : $default;
}
/**
* set a single attribute with the given $name to given $value
*
* @param string $name
* @param mixed $value
* @return self For a fluid interface.
*/
public function setAttribute($name, $value)
{
$this->attributes[$name] = $value;
return $this;
}
/**
* get an array of all attributes
*
* @return array
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* set an array of additional attributes
*
* @param array $attributes
* @return self For a fluid interface.
*/
public function setAttributes(array $attributes)
{
$this->attributes = $attributes + $this->attributes;
return $this;
}
/**
* get a container for all attributes
*
* @return AttributeBag
*/
public function getAttributeBag()
{
return $this;
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace Fhaculty\Graph\Attribute;
/**
* An attribute bag that automatically prefixes a given namespace.
*
* For example, you can use this class to prefix the attributes using a vendor
* name, like "myvendor.item.". If another vendor shares the base attribute
* bag, it can use a different prefix, like "otherProduct.item.". This allows
* both libraries to have attributes with the same name without having them
* conflict. For example, the attribute "id" would be stored separately as
* "myvendor.item.id" and "otherProduct.item.id".
*/
class AttributeBagNamespaced implements AttributeBag
{
/**
* @var AttributeBag
*/
private $bag;
/**
* @var string
*/
private $prefix;
/**
* Initialize the attribute bag with a prefix to use as a namespace for the attributes.
*
* @param AttributeAware $bag The bag to store the prefixed attributes in.
* @param string $prefix The prefix to prepend to all attributes before
* storage. This prefix acts as a namespace to separate attributes.
*/
public function __construct(AttributeAware $bag, $prefix)
{
if (!($bag instanceof AttributeBag)) {
$bag = $bag->getAttributeBag();
}
$this->bag = $bag;
$this->prefix = $prefix;
}
/**
* get a single attribute with the given $name (or return $default if attribute was not found)
*
* This prefixes the attribute name before requesting from the base bag.
*
* @param string $name
* @param mixed $default to return if attribute was not found
* @return mixed
*/
public function getAttribute($name, $default = null)
{
return $this->bag->getAttribute($this->prefix . $name, $default);
}
/**
* set a single attribute with the given $name to given $value
*
* This prefixes the attribute name before setting in the base bag.
*
* @param string $name
* @param mixed $value
* @return void
*/
public function setAttribute($name, $value)
{
$this->bag->setAttribute($this->prefix . $name, $value);
}
/**
* get an array of all attributes
*
* The prefix will not be included in the returned attribute keys.
*
* @return array
*/
public function getAttributes()
{
$attributes = array();
$len = strlen($this->prefix);
foreach ($this->bag->getAttributes() as $name => $value) {
if (strpos($name, $this->prefix) === 0) {
$attributes[substr($name, $len)] = $value;
}
}
return $attributes;
}
/**
* set an array of additional attributes
*
* Each attribute is prefixed before setting in the base bag.
*
* @param array $attributes
* @return void
*/
public function setAttributes(array $attributes)
{
foreach ($attributes as $name => $value) {
$this->bag->setAttribute($this->prefix . $name, $value);
}
}
/**
* get a container for all attributes
*
* @return AttributeBag
*/
public function getAttributeBag()
{
return $this;
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Fhaculty\Graph\Attribute;
/**
* The basic attribute bag, but using a reference to the base attribute array.
*
* This container passes and returns attributes by value, but stores them in a
* pass-by-reference array. It is mutable, however, so multiple references to
* the container will update in kind.
*/
class AttributeBagReference implements AttributeBag
{
/**
* @var array
*/
private $attributes;
/**
* Initialize the attribute bag with the base attribute array.
*
* The given array is pass-by-reference, so updates to the array here or in
* calling code will be reflected everywhere.
*
* @param array $attributes The pass-by-reference attributes.
*/
public function __construct(array &$attributes)
{
$this->attributes =& $attributes;
}
/**
* get a single attribute with the given $name (or return $default if attribute was not found)
*
* @param string $name
* @param mixed $default to return if attribute was not found
* @return mixed
*/
public function getAttribute($name, $default = null)
{
return isset($this->attributes[$name]) ? $this->attributes[$name] : $default;
}
/**
* set a single attribute with the given $name to given $value
*
* @param string $name
* @param mixed $value
* @return self For a fluid interface.
*/
public function setAttribute($name, $value)
{
$this->attributes[$name] = $value;
return $this;
}
/**
* get an array of all attributes
*
* @return array
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* set an array of additional attributes
*
* @param array $attributes
* @return self For a fluid interface.
*/
public function setAttributes(array $attributes)
{
$this->attributes = $attributes + $this->attributes;
return $this;
}
/**
* get a container for all attributes
*
* @return AttributeBag
*/
public function getAttributeBag()
{
return $this;
}
}

308
vendor/clue/graph/src/Edge/Base.php vendored Normal file
View File

@@ -0,0 +1,308 @@
<?php
namespace Fhaculty\Graph\Edge;
use Fhaculty\Graph\Attribute\AttributeAware;
use Fhaculty\Graph\Attribute\AttributeBagReference;
use Fhaculty\Graph\Exception\BadMethodCallException;
use Fhaculty\Graph\Exception\InvalidArgumentException;
use Fhaculty\Graph\Exception\LogicException;
use Fhaculty\Graph\Exception\RangeException;
use Fhaculty\Graph\Graph;
use Fhaculty\Graph\Set\Vertices;
use Fhaculty\Graph\Set\VerticesAggregate;
use Fhaculty\Graph\Vertex;
abstract class Base implements VerticesAggregate, AttributeAware
{
/**
* weight of this edge
*
* @var float|int|NULL
* @see self::getWeight()
*/
protected $weight = NULL;
/**
* maximum capacity (maximum flow)
*
* @var float|int|NULL
* @see self::getCapacity()
*/
protected $capacity = NULL;
/**
* flow (capacity currently in use)
*
* @var float|int|NULL
* @see self::getFlow()
*/
protected $flow = NULL;
protected $attributes = array();
/**
* get Vertices that are a target of this edge
*
* @return Vertices
*/
abstract public function getVerticesTarget();
/**
* get Vertices that are the start of this edge
*
* @return Vertices
*/
abstract public function getVerticesStart();
/**
* return true if this edge is an outgoing edge of the given vertex (i.e. the given vertex is a valid start vertex of this edge)
*
* @param Vertex $startVertex
* @return bool
* @uses Vertex::getVertexToFrom()
*/
abstract public function hasVertexStart(Vertex $startVertex);
/**
* return true if this edge is an ingoing edge of the given vertex (i . e. the given vertex is a valid end vertex of this edge)
*
* @param Vertex $targetVertex
* @return bool
* @uses Vertex::getVertexFromTo()
*/
abstract function hasVertexTarget(Vertex $targetVertex);
abstract public function isConnection(Vertex $from, Vertex $to);
/**
* returns whether this edge is actually a loop
*
* @return bool
*/
abstract public function isLoop();
/**
* get target vertex we can reach with this edge from the given start vertex
*
* @param Vertex $startVertex
* @return Vertex
* @throws InvalidArgumentException if given $startVertex is not a valid start
* @see self::hasEdgeFrom() to check if given start is valid
*/
abstract public function getVertexToFrom(Vertex $startVertex);
/**
* get start vertex which can reach us(the given end vertex) with this edge
*
* @param Vertex $endVertex
* @return Vertex
* @throws InvalidArgumentException if given $startVertex is not a valid end
* @see self::hasEdgeFrom() to check if given start is valid
*/
abstract public function getVertexFromTo(Vertex $endVertex);
/**
* return weight of edge
*
* @return float|int|NULL numeric weight of edge or NULL=not set
*/
public function getWeight()
{
return $this->weight;
}
/**
* set new weight for edge
*
* @param float|int|NULL $weight new numeric weight of edge or NULL=unset weight
* @return self $this (chainable)
* @throws InvalidArgumentException if given weight is not numeric
*/
public function setWeight($weight)
{
if ($weight !== NULL && !is_float($weight) && !is_int($weight)) {
throw new InvalidArgumentException('Invalid weight given - must be numeric or NULL');
}
$this->weight = $weight;
return $this;
}
/**
* get total capacity of this edge
*
* @return float|int|NULL numeric capacity or NULL=not set
*/
public function getCapacity()
{
return $this->capacity;
}
/**
* get the capacity remaining (total capacity - current flow)
*
* @return float|int|NULL numeric capacity remaining or NULL=no upper capacity set
*/
public function getCapacityRemaining()
{
if ($this->capacity === NULL) {
return NULL;
}
return $this->capacity - $this->flow;
}
/**
* set new total capacity of this edge
*
* @param float|int|NULL $capacity
* @return self $this (chainable)
* @throws InvalidArgumentException if $capacity is invalid (not numeric or negative)
* @throws RangeException if current flow exceeds new capacity
*/
public function setCapacity($capacity)
{
if ($capacity !== NULL) {
if (!is_float($capacity) && !is_int($capacity)) {
throw new InvalidArgumentException('Invalid capacity given - must be numeric');
}
if ($capacity < 0) {
throw new InvalidArgumentException('Capacity must not be negative');
}
if ($this->flow !== NULL && $this->flow > $capacity) {
throw new RangeException('Current flow of ' . $this->flow . ' exceeds new capacity');
}
}
$this->capacity = $capacity;
return $this;
}
/**
* get current flow (capacity currently in use)
*
* @return float|int|NULL numeric flow or NULL=not set
*/
public function getFlow()
{
return $this->flow;
}
/**
* set new total flow (capacity currently in use)
*
* @param float|int|NULL $flow
* @return self $this (chainable)
* @throws InvalidArgumentException if $flow is invalid (not numeric or negative)
* @throws RangeException if flow exceeds current maximum capacity
*/
public function setFlow($flow)
{
if ($flow !== NULL) {
if (!is_float($flow) && !is_int($flow)) {
throw new InvalidArgumentException('Invalid flow given - must be numeric');
}
if ($flow < 0) {
throw new InvalidArgumentException('Flow must not be negative');
}
if ($this->capacity !== NULL && $flow > $this->capacity) {
throw new RangeException('New flow exceeds maximum capacity');
}
}
$this->flow = $flow;
return $this;
}
/**
* get set of all Vertices this edge connects
*
* @return Vertices
*/
//abstract public function getVertices();
/**
* get graph instance this edge is attached to
*
* @return Graph
* @throws LogicException
*/
public function getGraph()
{
foreach ($this->getVertices() as $vertex) {
return $vertex->getGraph();
// the following code can only be reached if this edge does not
// contain any vertices (invalid state), so ignore its coverage
// @codeCoverageIgnoreStart
}
throw new LogicException('Internal error: should not be reached');
// @codeCoverageIgnoreEnd
}
/**
* destroy edge and remove reference from vertices and graph
*
* @uses Graph::removeEdge()
* @uses Vertex::removeEdge()
* @return void
*/
public function destroy()
{
$this->getGraph()->removeEdge($this);
foreach ($this->getVertices() as $vertex) {
$vertex->removeEdge($this);
}
}
/**
* create new clone of this edge between adjacent vertices
*
* @return self new edge
* @uses Graph::createEdgeClone()
*/
public function createEdgeClone()
{
return $this->getGraph()->createEdgeClone($this);
}
/**
* create new clone of this edge inverted (in opposite direction) between adjacent vertices
*
* @return self new edge
* @uses Graph::createEdgeCloneInverted()
*/
public function createEdgeCloneInverted()
{
return $this->getGraph()->createEdgeCloneInverted($this);
}
/**
* do NOT allow cloning of objects
*
* @throws BadMethodCallException
*/
private function __clone()
{
// @codeCoverageIgnoreStart
throw new BadMethodCallException();
// @codeCoverageIgnoreEnd
}
public function getAttribute($name, $default = null)
{
return isset($this->attributes[$name]) ? $this->attributes[$name] : $default;
}
public function setAttribute($name, $value)
{
$this->attributes[$name] = $value;
}
public function getAttributeBag()
{
return new AttributeBagReference($this->attributes);
}
}

119
vendor/clue/graph/src/Edge/Directed.php vendored Normal file
View File

@@ -0,0 +1,119 @@
<?php
namespace Fhaculty\Graph\Edge;
use Fhaculty\Graph\Exception\InvalidArgumentException;
use Fhaculty\Graph\Set\Vertices;
use Fhaculty\Graph\Vertex;
class Directed extends Base
{
/**
* source/start vertex
*
* @var Vertex
*/
private $from;
/**
* target/end vertex
*
* @var Vertex
*/
private $to;
/**
* create a new directed Edge from Vertex $from to Vertex $to
*
* @param Vertex $from start/source Vertex
* @param Vertex $to end/target Vertex
* @see Vertex::createEdgeTo() to create directed edges
* @see Vertex::createEdge() to create undirected edges
*/
public function __construct(Vertex $from, Vertex $to)
{
if ($from->getGraph() !== $to->getGraph()) {
throw new InvalidArgumentException('Vertices have to be within the same graph');
}
$this->from = $from;
$this->to = $to;
$from->getGraph()->addEdge($this);
$from->addEdge($this);
$to->addEdge($this);
}
public function getVerticesTarget()
{
return new Vertices(array($this->to));
}
public function getVerticesStart()
{
return new Vertices(array($this->from));
}
public function getVertices()
{
return new Vertices(array($this->from, $this->to));
}
/**
* get end/target vertex
*
* @return Vertex
*/
public function getVertexEnd()
{
return $this->to;
}
/**
* get start vertex
*
* @return Vertex
*/
public function getVertexStart()
{
return $this->from;
}
public function isConnection(Vertex $from, Vertex $to)
{
return ($this->to === $to && $this->from === $from);
}
public function isLoop()
{
return ($this->to === $this->from);
}
public function getVertexToFrom(Vertex $startVertex)
{
if ($this->from !== $startVertex) {
throw new InvalidArgumentException('Invalid start vertex');
}
return $this->to;
}
public function getVertexFromTo(Vertex $endVertex)
{
if ($this->to !== $endVertex) {
throw new InvalidArgumentException('Invalid end vertex');
}
return $this->from;
}
public function hasVertexStart(Vertex $startVertex)
{
return ($this->from === $startVertex);
}
public function hasVertexTarget(Vertex $targetVertex)
{
return ($this->to === $targetVertex);
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace Fhaculty\Graph\Edge;
use Fhaculty\Graph\Exception\InvalidArgumentException;
use Fhaculty\Graph\Vertex;
use Fhaculty\Graph\Set\Vertices;
class Undirected extends Base
{
/**
* vertex a
*
* @var Vertex
*/
private $a;
/**
* vertex b
*
* @var Vertex
*/
private $b;
/**
* create a new undirected edge between given vertices
*
* @param Vertex $a
* @param Vertex $b
* @see Vertex::createEdge() instead
*/
public function __construct(Vertex $a, Vertex $b)
{
if ($a->getGraph() !== $b->getGraph()) {
throw new InvalidArgumentException('Vertices have to be within the same graph');
}
$this->a = $a;
$this->b = $b;
$a->getGraph()->addEdge($this);
$a->addEdge($this);
$b->addEdge($this);
}
public function getVerticesTarget()
{
return new Vertices(array($this->b, $this->a));
}
public function getVerticesStart()
{
return new Vertices(array($this->a, $this->b));
}
public function getVertices()
{
return new Vertices(array($this->a, $this->b));
}
public function isConnection(Vertex $from, Vertex $to)
{
// one way or other way
return (($this->a === $from && $this->b === $to) || ($this->b === $from && $this->a === $to));
}
public function isLoop()
{
return ($this->a === $this->b);
}
public function getVertexToFrom(Vertex $startVertex)
{
if ($this->a === $startVertex) {
return $this->b;
} elseif ($this->b === $startVertex) {
return $this->a;
} else {
throw new InvalidArgumentException('Invalid start vertex');
}
}
public function getVertexFromTo(Vertex $endVertex)
{
if ($this->a === $endVertex) {
return $this->b;
} elseif ($this->b === $endVertex) {
return $this->a;
} else {
throw new InvalidArgumentException('Invalid end vertex');
}
}
public function hasVertexStart(Vertex $startVertex)
{
return ($this->a === $startVertex || $this->b === $startVertex);
}
public function hasVertexTarget(Vertex $targetVertex)
{
// same implementation as direction does not matter
return $this->hasVertexStart($targetVertex);
}
}

7
vendor/clue/graph/src/Exception.php vendored Normal file
View File

@@ -0,0 +1,7 @@
<?php
namespace Fhaculty\Graph;
interface Exception
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Fhaculty\Graph\Exception;
use Fhaculty\Graph;
class BadMethodCallException extends \BadMethodCallException implements Graph\Exception
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Fhaculty\Graph\Exception;
use Fhaculty\Graph;
class DomainException extends \DomainException implements Graph\Exception
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Fhaculty\Graph\Exception;
use Fhaculty\Graph;
class InvalidArgumentException extends \InvalidArgumentException implements Graph\Exception
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Fhaculty\Graph\Exception;
use Fhaculty\Graph;
class LogicException extends \LogicException implements Graph\Exception
{
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Fhaculty\Graph\Exception;
use Fhaculty\Graph\Walk;
use Fhaculty\Graph;
class NegativeCycleException extends UnexpectedValueException implements Graph\Exception
{
/**
* instance of the cycle
*
* @var Walk
*/
private $cycle;
public function __construct($message, $code = NULL, $previous = NULL, Walk $cycle = null)
{
// $cycle is required, but required argument may not appear after option arguments as of PHP 8
if ($cycle === null) {
throw new \InvalidArgumentException('Missing required cycle');
}
parent::__construct($message, $code, $previous);
$this->cycle = $cycle;
}
/**
*
* @return Walk
*/
public function getCycle()
{
return $this->cycle;
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Fhaculty\Graph\Exception;
use Fhaculty\Graph;
class OutOfBoundsException extends \OutOfBoundsException implements Graph\Exception
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Fhaculty\Graph\Exception;
use Fhaculty\Graph;
class OverflowException extends \OverflowException implements Graph\Exception
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Fhaculty\Graph\Exception;
use Fhaculty\Graph;
class RangeException extends \RangeException implements Graph\Exception
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Fhaculty\Graph\Exception;
use Fhaculty\Graph;
class RuntimeException extends \RuntimeException implements Graph\Exception
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Fhaculty\Graph\Exception;
use Fhaculty\Graph;
class UnderflowException extends \UnderflowException implements Graph\Exception
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Fhaculty\Graph\Exception;
use Fhaculty\Graph;
class UnexpectedValueException extends \UnexpectedValueException implements Graph\Exception
{
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Fhaculty\Graph\Exporter;
use Fhaculty\Graph\Graph;
interface ExporterInterface
{
public function getOutput(Graph $graph);
}

464
vendor/clue/graph/src/Graph.php vendored Normal file
View File

@@ -0,0 +1,464 @@
<?php
namespace Fhaculty\Graph;
use Fhaculty\Graph\Attribute\AttributeAware;
use Fhaculty\Graph\Attribute\AttributeBagReference;
use Fhaculty\Graph\Edge\Base as Edge;
use Fhaculty\Graph\Edge\Directed as EdgeDirected;
use Fhaculty\Graph\Exception\BadMethodCallException;
use Fhaculty\Graph\Exception\InvalidArgumentException;
use Fhaculty\Graph\Exception\OutOfBoundsException;
use Fhaculty\Graph\Exception\OverflowException;
use Fhaculty\Graph\Exception\RuntimeException;
use Fhaculty\Graph\Exception\UnderflowException;
use Fhaculty\Graph\Set\DualAggregate;
use Fhaculty\Graph\Set\Edges;
use Fhaculty\Graph\Set\Vertices;
use Fhaculty\Graph\Set\VerticesMap;
class Graph implements DualAggregate, AttributeAware
{
protected $verticesStorage = array();
protected $vertices;
protected $edgesStorage = array();
protected $edges;
protected $attributes = array();
public function __construct()
{
$this->vertices = VerticesMap::factoryArrayReference($this->verticesStorage);
$this->edges = Edges::factoryArrayReference($this->edgesStorage);
}
/**
* return set of Vertices added to this graph
*
* @return Vertices
*/
public function getVertices()
{
return $this->vertices;
}
/**
* return set of ALL Edges added to this graph
*
* @return Edges
*/
public function getEdges()
{
return $this->edges;
}
/**
* create a new Vertex in the Graph
*
* @param int|NULL $id new vertex ID to use (defaults to NULL: use next free numeric ID)
* @param bool $returnDuplicate normal operation is to throw an exception if given id already exists. pass true to return original vertex instead
* @return Vertex (chainable)
* @throws InvalidArgumentException if given vertex $id is invalid
* @throws OverflowException if given vertex $id already exists and $returnDuplicate is not set
* @uses Vertex::getId()
*/
public function createVertex($id = NULL, $returnDuplicate = false)
{
// no ID given
if ($id === NULL) {
$id = $this->getNextId();
}
if ($returnDuplicate && $this->vertices->hasVertexId($id)) {
return $this->vertices->getVertexId($id);
}
return new Vertex($this, $id);
}
/**
* create a new Vertex in this Graph from the given input Vertex of another graph
*
* @param Vertex $originalVertex
* @return Vertex new vertex in this graph
* @throws RuntimeException if vertex with this ID already exists
*/
public function createVertexClone(Vertex $originalVertex)
{
$id = $originalVertex->getId();
if ($this->vertices->hasVertexId($id)) {
throw new RuntimeException('Id of cloned vertex already exists');
}
$newVertex = new Vertex($this, $id);
// TODO: properly set attributes of vertex
$newVertex->getAttributeBag()->setAttributes($originalVertex->getAttributeBag()->getAttributes());
$newVertex->setBalance($originalVertex->getBalance());
$newVertex->setGroup($originalVertex->getGroup());
return $newVertex;
}
/**
* create new clone/copy of this graph - copy all attributes and vertices, but do NOT copy edges
*
* using this method is faster than creating a new graph and calling createEdgeClone() yourself
*
* @return Graph
*/
public function createGraphCloneEdgeless()
{
$graph = new Graph();
$graph->getAttributeBag()->setAttributes($this->getAttributeBag()->getAttributes());
// TODO: set additional graph attributes
foreach ($this->getVertices() as $originalVertex) {
$vertex = $graph->createVertexClone($originalVertex);
// $graph->vertices[$vid] = $vertex;
}
return $graph;
}
/**
* create new clone/copy of this graph - copy all attributes and vertices. but only copy all given edges
*
* @param Edges|Edge[] $edges set or array of edges to be cloned
* @return Graph
* @uses Graph::createGraphCloneEdgeless()
* @uses Graph::createEdgeClone() for each edge to be cloned
*/
public function createGraphCloneEdges($edges)
{
$graph = $this->createGraphCloneEdgeless();
foreach ($edges as $edge) {
$graph->createEdgeClone($edge);
}
return $graph;
}
/**
* create new clone/copy of this graph - copy all attributes, vertices and edges
*
* @return Graph
* @uses Graph::createGraphCloneEdges() to clone graph with current edges
*/
public function createGraphClone()
{
return $this->createGraphCloneEdges($this->edges);
}
/**
* create a new clone/copy of this graph - copy all attributes and given vertices and its edges
*
* @param Vertices $vertices set of vertices to keep
* @return Graph
* @uses Graph::createGraphClone() to create a complete clone
* @uses Vertex::destroy() to remove unneeded vertices again
*/
public function createGraphCloneVertices($vertices)
{
$verticesKeep = Vertices::factory($vertices);
$graph = $this->createGraphClone();
foreach ($graph->getVertices()->getMap() as $vid => $vertex) {
if (!$verticesKeep->hasVertexId($vid)) {
$vertex->destroy();
}
}
return $graph;
}
/**
* create new clone of the given edge between adjacent vertices
*
* @param Edge $originalEdge original edge (not neccessarily from this graph)
* @return Edge new edge in this graph
* @uses Graph::createEdgeCloneInternal()
*/
public function createEdgeClone(Edge $originalEdge)
{
return $this->createEdgeCloneInternal($originalEdge, 0, 1);
}
/**
* create new clone of the given edge inverted (in opposite direction) between adjacent vertices
*
* @param Edge $originalEdge original edge (not neccessarily from this graph)
* @return Edge new edge in this graph
* @uses Graph::createEdgeCloneInternal()
*/
public function createEdgeCloneInverted(Edge $originalEdge)
{
return $this->createEdgeCloneInternal($originalEdge, 1, 0);
}
/**
* create new clone of the given edge between adjacent vertices
*
* @param Edge $originalEdge original edge from old graph
* @param int $ia index of start vertex
* @param int $ib index of end vertex
* @return Edge new edge in this graph
* @uses Edge::getVertices()
* @uses Graph::getVertex()
* @uses Vertex::createEdge() to create a new undirected edge if given edge was undrected
* @uses Vertex::createEdgeTo() to create a new directed edge if given edge was directed
* @uses Edge::getWeight()
* @uses Edge::setWeight()
* @uses Edge::getFlow()
* @uses Edge::setFlow()
* @uses Edge::getCapacity()
* @uses Edge::setCapacity()
*/
private function createEdgeCloneInternal(Edge $originalEdge, $ia, $ib)
{
$ends = $originalEdge->getVertices()->getIds();
// get start vertex from old start vertex id
$a = $this->getVertex($ends[$ia]);
// get target vertex from old target vertex id
$b = $this->getVertex($ends[$ib]);
if ($originalEdge instanceof EdgeDirected) {
$newEdge = $a->createEdgeTo($b);
} else {
// create new edge between new a and b
$newEdge = $a->createEdge($b);
}
// TODO: copy edge attributes
$newEdge->getAttributeBag()->setAttributes($originalEdge->getAttributeBag()->getAttributes());
$newEdge->setWeight($originalEdge->getWeight());
$newEdge->setFlow($originalEdge->getFlow());
$newEdge->setCapacity($originalEdge->getCapacity());
return $newEdge;
}
/**
* create the given number of vertices or given array of Vertex IDs
*
* @param int|array $n number of vertices to create or array of Vertex IDs to create
* @return Vertices set of Vertices created
* @uses Graph::getNextId()
*/
public function createVertices($n)
{
$vertices = array();
if (is_int($n) && $n >= 0) {
for ($id = $this->getNextId(), $n += $id; $id < $n; ++$id) {
$vertices[$id] = new Vertex($this, $id);
}
} elseif (is_array($n)) {
// array given => check to make sure all given IDs are available (atomic operation)
foreach ($n as $id) {
if (!is_int($id) && !is_string($id)) {
throw new InvalidArgumentException('All Vertex IDs have to be of type integer or string');
} elseif ($this->vertices->hasVertexId($id)) {
throw new OverflowException('Given array of Vertex IDs contains an ID that already exists. Given IDs must be unique');
} elseif (isset($vertices[$id])) {
throw new InvalidArgumentException('Given array of Vertex IDs contain duplicate IDs. Given IDs must be unique');
}
// temporary marker to check for duplicate IDs in the array
$vertices[$id] = false;
}
// actually create all requested vertices
foreach ($n as $id) {
$vertices[$id] = new Vertex($this, $id);
}
} else {
throw new InvalidArgumentException('Invalid number of vertices given. Must be non-negative integer or an array of Vertex IDs');
}
return new Vertices($vertices);
}
/**
* get next free/unused/available vertex ID
*
* its guaranteed there's NO other vertex with a greater ID
*
* @return int
*/
private function getNextId()
{
if (!$this->verticesStorage) {
return 0;
}
// auto ID
return max(array_map('intval', array_keys($this->verticesStorage)))+1;
}
/**
* returns the Vertex with identifier $id
*
* @param int|string $id identifier of Vertex
* @return Vertex
* @throws OutOfBoundsException if given vertex ID does not exist
*/
public function getVertex($id)
{
return $this->vertices->getVertexId($id);
}
/**
* checks whether given vertex ID exists in this graph
*
* @param int|string $id identifier of Vertex
* @return bool
*/
public function hasVertex($id)
{
return $this->vertices->hasVertexId($id);
}
/**
* adds a new Vertex to the Graph (MUST NOT be called manually!)
*
* @param Vertex $vertex instance of the new Vertex
* @return void
* @internal
* @see self::createVertex() instead!
*/
public function addVertex(Vertex $vertex)
{
if (isset($this->verticesStorage[$vertex->getId()])) {
throw new OverflowException('ID must be unique');
}
$this->verticesStorage[$vertex->getId()] = $vertex;
}
/**
* adds a new Edge to the Graph (MUST NOT be called manually!)
*
* @param Edge $edge instance of the new Edge
* @return void
* @internal
* @see Vertex::createEdge() instead!
*/
public function addEdge(Edge $edge)
{
$this->edgesStorage []= $edge;
}
/**
* remove the given edge from list of connected edges (MUST NOT be called manually!)
*
* @param Edge $edge
* @return void
* @throws InvalidArgumentException if given edge does not exist (should not ever happen)
* @internal
* @see Edge::destroy() instead!
*/
public function removeEdge(Edge $edge)
{
try {
unset($this->edgesStorage[$this->edges->getIndexEdge($edge)]);
}
catch (OutOfBoundsException $e) {
throw new InvalidArgumentException('Invalid Edge does not exist in this Graph');
}
}
/**
* remove the given vertex from list of known vertices (MUST NOT be called manually!)
*
* @param Vertex $vertex
* @return void
* @throws InvalidArgumentException if given vertex does not exist (should not ever happen)
* @internal
* @see Vertex::destroy() instead!
*/
public function removeVertex(Vertex $vertex)
{
try {
unset($this->verticesStorage[$this->vertices->getIndexVertex($vertex)]);
}
catch (OutOfBoundsException $e) {
throw new InvalidArgumentException('Invalid Vertex does not exist in this Graph');
}
}
/**
* Extracts edge from this graph
*
* @param Edge $edge
* @return Edge
* @throws UnderflowException if no edge was found
* @throws OverflowException if multiple edges match
*/
public function getEdgeClone(Edge $edge)
{
// Extract endpoints from edge
$vertices = $edge->getVertices()->getVector();
return $this->getEdgeCloneInternal($edge, $vertices[0], $vertices[1]);
}
/**
* Extracts inverted edge from this graph
*
* @param Edge $edge
* @return Edge
* @throws UnderflowException if no edge was found
* @throws OverflowException if multiple edges match
*/
public function getEdgeCloneInverted(Edge $edge)
{
// Extract endpoints from edge
$vertices = $edge->getVertices()->getVector();
return $this->getEdgeCloneInternal($edge, $vertices[1], $vertices[0]);
}
private function getEdgeCloneInternal(Edge $edge, Vertex $startVertex, Vertex $targetVertex)
{
// Get original vertices from resultgraph
$residualGraphEdgeStartVertex = $this->getVertex($startVertex->getId());
$residualGraphEdgeTargetVertex = $this->getVertex($targetVertex->getId());
// Now get the edge
$residualEdgeArray = $residualGraphEdgeStartVertex->getEdgesTo($residualGraphEdgeTargetVertex);
$residualEdgeArray = Edges::factory($residualEdgeArray)->getVector();
// Check for parallel edges
if (!$residualEdgeArray) {
throw new UnderflowException('No original edges for given cloned edge found');
} elseif (count($residualEdgeArray) !== 1) {
throw new OverflowException('More than one cloned edge? Parallel edges (multigraph) not supported');
}
return $residualEdgeArray[0];
}
/**
* do NOT allow cloning of objects (MUST NOT be called!)
*
* @throws BadMethodCallException
* @see Graph::createGraphClone() instead
*/
private function __clone()
{
// @codeCoverageIgnoreStart
throw new BadMethodCallException();
// @codeCoverageIgnoreEnd
}
public function getAttribute($name, $default = null)
{
return isset($this->attributes[$name]) ? $this->attributes[$name] : $default;
}
public function setAttribute($name, $value)
{
$this->attributes[$name] = $value;
}
public function getAttributeBag()
{
return new AttributeBagReference($this->attributes);
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Fhaculty\Graph\Set;
/**
* A DualAggregate provides access to both its Vertices and its Edges
*
* This is the simple base interface for any Graph-like structure / data type
* which contains a Set of Edges and a Set of Vertices, such as the Graph class
* itself and the Walk class.
*/
interface DualAggregate extends VerticesAggregate, EdgesAggregate
{
/**
* returns a set of ALL Edges in this graph
*
* @return Edges
*/
// abstract public function getEdges();
/**
* returns a set of all Vertices
*
* @return Vertices
*/
// abstract public function getVertices();
}

487
vendor/clue/graph/src/Set/Edges.php vendored Normal file
View File

@@ -0,0 +1,487 @@
<?php
namespace Fhaculty\Graph\Set;
use Fhaculty\Graph\Edge\Base as Edge;
use Fhaculty\Graph\Exception\InvalidArgumentException;
use Fhaculty\Graph\Exception\OutOfBoundsException;
use Fhaculty\Graph\Exception\UnderflowException;
/**
* A Set of Edges
*
* Contains any number of Edge (directed and/or undirected) instances.
*
* The Set is a readonly instance and it provides methods to get single Edge
* instances or to get a new Set of Edges. This way it's safe to pass around
* the original Set of Edges, because it will never be modified.
*/
class Edges implements \Countable, \IteratorAggregate, EdgesAggregate
{
/**
* order by edge weight
*
* @var int
* @see Edge::getWeight()
*/
const ORDER_WEIGHT = 1;
/**
* order by edge capacity
*
* @var int
* @see Edge::getCapacity()
*/
const ORDER_CAPACITY = 2;
/**
* order by remaining capacity on edge (maximum capacity - current flow)
*
* @var int
* @see Edge::getCapacityRemaining()
*/
const ORDER_CAPACITY_REMAINING = 3;
/**
* order by edge flow
*
* @var int
* @see Edge::getFlow()
*/
const ORDER_FLOW = 4;
/**
* random/shuffled order
*
* @var int
*/
const ORDER_RANDOM = 5;
protected $edges = array();
/**
* create new Edges instance
*
* You can pass in just about anything that can be expressed as a Set of
* Edges, such as:
* - an array of Edge instances
* - any Algorithm that implements the EdgesAggregate interface
* - a Graph instance or
* - an existing Set of Edges which will be returned as-is
*
* @param array|Edges|EdgesAggregate $edges
* @return Edges
*/
public static function factory($edges)
{
if ($edges instanceof EdgesAggregate) {
return $edges->getEdges();
}
return new self($edges);
}
/**
* create new Edges instance that references the given source array of Edge instances
*
* Any changes in the referenced source array will automatically be
* reflected in this Set of Edges, e.g. if you add an Edge instance to the
* array, it will automatically be included in this Set.
*
* @param array $edgesArray
* @return Edges
*/
public static function factoryArrayReference(array &$edgesArray)
{
$edges = new static();
$edges->edges =& $edgesArray;
return $edges;
}
/**
* instantiate new Set of Edges
*
* @param array $edges
*/
public function __construct(array $edges = array())
{
$this->edges = $edges;
}
/**
* get array index for given Edge
*
* @param Edge $edge
* @throws OutOfBoundsException
* @return mixed
*/
public function getIndexEdge(Edge $edge)
{
$id = array_search($edge, $this->edges, true);
if ($id === false) {
throw new OutOfBoundsException('Given edge does NOT exist');
}
return $id;
}
/**
* return first Edge in this set of Edges
*
* some algorithms do not need a particular edge, but merely a (random)
* starting point. this is a convenience function to just pick the first
* edge from the list of known edges.
*
* @return Edge first Edge in this set of Edges
* @throws UnderflowException if set is empty
* @see self::getEdgeOrder() if you need to apply ordering first
*/
public function getEdgeFirst()
{
if (!$this->edges) {
throw new UnderflowException('Does not contain any edges');
}
reset($this->edges);
return current($this->edges);
}
/**
* return last Edge in this set of Edges
*
* @return Edge last Edge in this set of Edges
* @throws UnderflowException if set is empty
*/
public function getEdgeLast()
{
if (!$this->edges) {
throw new UnderflowException('Does not contain any edges');
}
end($this->edges);
return current($this->edges);
}
/**
* return Edge at given array index
*
* @param mixed $index
* @throws OutOfBoundsException if the given index does not exist
* @return Edge
*/
public function getEdgeIndex($index)
{
if (!isset($this->edges[$index])) {
throw new OutOfBoundsException('Invalid edge index');
}
return $this->edges[$index];
}
/**
* return first Edge that matches the given callback filter function
*
* @param callable $callbackCheck
* @return Edge
* @throws UnderflowException if no Edge matches the given callback filter function
* @uses self::getEdgeMatchOrNull()
* @see self::getEdgesMatch() if you want to return *all* Edges that match
*/
public function getEdgeMatch($callbackCheck)
{
$ret = $this->getEdgeMatchOrNull($callbackCheck);
if ($ret === null) {
throw new UnderflowException('No edge found');
}
return $ret;
}
/**
* checks whethere there's an Edge that matches the given callback filter function
*
* @param callable $callbackCheck
* @return bool
* @see self::getEdgeMatch() to return the Edge instance that matches the given callback filter function
* @uses self::getEdgeMatchOrNull()
*/
public function hasEdgeMatch($callbackCheck)
{
return ($this->getEdgeMatchOrNull($callbackCheck) !== null);
}
/**
* get a new set of Edges that match the given callback filter function
*
* This only keeps Edge elements if the $callbackCheck returns a bool
* true and filters out everything else.
*
* Edge index positions will be left unchanged.
*
* @param callable $callbackCheck
* @return Edges a new Edges instance
* @see self::getEdgeMatch()
*/
public function getEdgesMatch($callbackCheck)
{
return new static(array_filter($this->edges, $callbackCheck));
}
/**
* get new set of Edges ordered by given criterium $orderBy
*
* Edge index positions will be left unchanged.
*
* @param int $orderBy criterium to sort by. see self::ORDER_WEIGHT, etc.
* @param bool $desc whether to return biggest first (true) instead of smallest first (default:false)
* @return Edges a new Edges set ordered by the given $orderBy criterium
* @throws InvalidArgumentException if criterium is unknown
*/
public function getEdgesOrder($orderBy, $desc = false)
{
if ($orderBy === self::ORDER_RANDOM) {
// shuffle the edge positions
$keys = array_keys($this->edges);
shuffle($keys);
// re-order according to shuffled edge positions
$edges = array();
foreach ($keys as $key) {
$edges[$key] = $this->edges[$key];
}
// create iterator for shuffled array (no need to check DESC flag)
return new static($edges);
}
$callback = $this->getCallback($orderBy);
$array = $this->edges;
uasort($array, function (Edge $va, Edge $vb) use ($callback, $desc) {
$ra = $callback($desc ? $vb : $va);
$rb = $callback($desc ? $va : $vb);
if ($ra < $rb) {
return -1;
} elseif ($ra > $rb) {
return 1;
} else {
return 0;
}
});
return new static($array);
}
/**
* get first edge ordered by given criterium $orderBy
*
* @param int $orderBy criterium to sort by. see self::ORDER_WEIGHT, etc.
* @param bool $desc whether to return biggest (true) instead of smallest (default:false)
* @return Edge
* @throws InvalidArgumentException if criterium is unknown
* @throws UnderflowException if no edges exist
*/
public function getEdgeOrder($orderBy, $desc=false)
{
if (!$this->edges) {
throw new UnderflowException('No edge found');
}
// random order
if ($orderBy === self::ORDER_RANDOM) {
// just return by random key (no need to check for DESC flag)
return $this->edges[array_rand($this->edges)];
}
$callback = $this->getCallback($orderBy);
$ret = NULL;
$best = NULL;
foreach ($this->edges as $edge) {
$now = $callback($edge);
if ($ret === NULL || ($desc && $now > $best) || (!$desc && $now < $best)) {
$ret = $edge;
$best = $now;
}
}
return $ret;
}
/**
* return self reference to Set of Edges
*
* @return Edges
* @see self::factory()
*/
public function getEdges()
{
return $this;
}
/**
* get a new set of Edges where each Edge is distinct/unique
*
* @return Edges a new Edges instance
*/
public function getEdgesDistinct()
{
$edges = array();
foreach ($this->edges as $edge) {
// filter duplicate edges
if (!in_array($edge, $edges, true)) {
$edges []= $edge;
}
}
return new Edges($edges);
}
/**
* get intersection of Edges with given other Edges
*
* The intersection contains all Edge instances that are present in BOTH
* this set of Edges and the given set of other Edges.
*
* Edge index/keys will be preserved from original array.
*
* Duplicate Edge instances will be kept if the corresponding number of
* Edge instances is also found in $otherEdges.
*
* @param Edges|Edge[] $otherEdges
* @return Edges a new Edges set
*/
public function getEdgesIntersection($otherEdges)
{
$otherArray = self::factory($otherEdges)->getVector();
$edges = array();
foreach ($this->edges as $eid => $edge) {
$i = array_search($edge, $otherArray, true);
if ($i !== false) {
// remove from other array in order to check for duplicate matches
unset($otherArray[$i]);
$edges[$eid] = $edge;
}
}
return new static($edges);
}
/**
* return array of Edge instances
*
* @return Edge[]
*/
public function getVector()
{
return array_values($this->edges);
}
/**
* count number of Edges
*
* @return int
* @see self::isEmpty()
*/
#[\ReturnTypeWillChange]
public function count()
{
return count($this->edges);
}
/**
* check whether this Set of Edges is empty
*
* A Set if empty if no single Edge instance is added. This is faster
* than calling `count() === 0`.
*
* @return bool
*/
public function isEmpty()
{
return !$this->edges;
}
/**
* get Iterator
*
* This method implements the IteratorAggregate interface and allows this
* Set of Edges to be used in foreach loops.
*
* @return \IteratorIterator
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
return new \IteratorIterator(new \ArrayIterator($this->edges));
}
/**
* call given $callback on each Edge and sum their results
*
* @param callable $callback
* @return number
* @throws InvalidArgumentException for invalid callbacks
* @uses self::getCallback()
*/
public function getSumCallback($callback)
{
$callback = $this->getCallback($callback);
// return array_sum(array_map($callback, $this->edges));
$sum = 0;
foreach ($this->edges as $edge) {
$sum += $callback($edge);
}
return $sum;
}
private function getEdgeMatchOrNull($callbackCheck)
{
$callbackCheck = $this->getCallback($callbackCheck);
foreach ($this->edges as $edge) {
if ($callbackCheck($edge)) {
return $edge;
}
}
return null;
}
/**
* get callback/Closure to be called on Edge instances for given callback identifier
*
* @param callable|int $callback
* @throws InvalidArgumentException
* @return callable
*/
private function getCallback($callback)
{
if (is_callable($callback)) {
if (is_array($callback)) {
$callback = function (Edge $edge) use ($callback) {
return call_user_func($callback, $edge);
};
}
return $callback;
}
static $methods = array(
self::ORDER_WEIGHT => 'getWeight',
self::ORDER_CAPACITY => 'getCapacity',
self::ORDER_CAPACITY_REMAINING => 'getCapacityRemaining',
self::ORDER_FLOW => 'getFlow'
);
if (!is_int($callback) || !isset($methods[$callback])) {
throw new InvalidArgumentException('Invalid callback given');
}
$method = $methods[$callback];
return function (Edge $edge) use ($method) {
return $edge->$method();
};
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Fhaculty\Graph\Set;
/**
* A basic interface for every class that provides access to its Set of Edges
*/
interface EdgesAggregate
{
/**
* @return Edges
*/
public function getEdges();
}

531
vendor/clue/graph/src/Set/Vertices.php vendored Normal file
View File

@@ -0,0 +1,531 @@
<?php
namespace Fhaculty\Graph\Set;
use Fhaculty\Graph\Vertex;
use Fhaculty\Graph\Exception\InvalidArgumentException;
use Fhaculty\Graph\Exception\OutOfBoundsException;
use Fhaculty\Graph\Exception\UnderflowException;
/**
* A Set of Vertices
*
* Contains any number of Vertex instances.
*
* The Set is a readonly instance and it provides methods to get single Vertex
* instances or to get a new Set of Vertices. This way it's safe to pass around
* the original Set of Vertices, because it will never be modified.
*/
class Vertices implements \Countable, \IteratorAggregate, VerticesAggregate
{
/**
* order by vertex ID
*
* @var int
* @see Vertex::getId()
*/
const ORDER_ID = 1;
/**
* random/shuffled order
*
* @var int
*/
const ORDER_RANDOM = 5;
/**
* order by vertex group
*
* @var int
* @see Vertex::getGroup()
*/
const ORDER_GROUP = 6;
protected $vertices = array();
/**
* create new Vertices instance
*
* You can pass in just about anything that can be expressed as a Set of
* Vertices, such as:
* - an array of Vertex instances
* - any Algorithm that implements the VerticesAggregate interface
* - a Graph instance or
* - an existing Set of Vertices which will be returned as-is
*
* @param array|Vertices|VerticesAggregate $vertices
* @return Vertices
*/
public static function factory($vertices)
{
if ($vertices instanceof VerticesAggregate) {
return $vertices->getVertices();
}
return new self($vertices);
}
/**
* create new Vertices instance that references the given source array of Vertex instances
*
* Any changes in the referenced source array will automatically be
* reflected in this Set of Vertices, e.g. if you add a Vertex instance to
* the array, it will automatically be included in this Set.
*
* @param array $verticesArray
* @return Vertices
*/
public static function factoryArrayReference(array &$verticesArray)
{
$vertices = new static();
$vertices->vertices =& $verticesArray;
return $vertices;
}
/**
* instantiate new Set of Vertices
*
* @param array $vertices
*/
public function __construct(array $vertices = array())
{
$this->vertices = $vertices;
}
/**
* get Vertex with the given vertex $id
*
* @param int|string $id
* @return Vertex
* @throws OutOfBoundsException if no Vertex with the given ID exists
* @uses self::getVertexMatch()
*/
public function getVertexId($id)
{
try {
return $this->getVertexMatch($this->getCallbackId($id));
}
catch (UnderflowException $e) {
throw new OutOfBoundsException('Vertex ' . $id . ' does not exist', 0, $e);
}
}
/**
* checks whether given vertex ID exists in this set of vertices
*
* @param int|string $id identifier of Vertex
* @return bool
* @uses self::hasVertexMatch()
*/
public function hasVertexId($id)
{
return $this->hasVertexMatch($this->getCallbackId($id));
}
/**
* get array index for given Vertex
*
* not every set of Vertices represents a map, as such array index and
* Vertex ID do not necessarily have to match.
*
* @param Vertex $vertex
* @throws OutOfBoundsException
* @return mixed
*/
public function getIndexVertex(Vertex $vertex)
{
$id = array_search($vertex, $this->vertices, true);
if ($id === false) {
throw new OutOfBoundsException('Given vertex does NOT exist');
}
return $id;
}
/**
* return first Vertex in this set of Vertices
*
* some algorithms do not need a particular vertex, but merely a (random)
* starting point. this is a convenience function to just pick the first
* vertex from the list of known vertices.
*
* @return Vertex first Vertex in this set of Vertices
* @throws UnderflowException if set is empty
* @see self::getVertexOrder() if you need to apply ordering first
*/
public function getVertexFirst()
{
if (!$this->vertices) {
throw new UnderflowException('Does not contain any vertices');
}
reset($this->vertices);
return current($this->vertices);
}
/**
* return last Vertex in this set of Vertices
*
* @return Vertex last Vertex in this set of Vertices
* @throws UnderflowException if set is empty
*/
public function getVertexLast()
{
if (!$this->vertices) {
throw new UnderflowException('Does not contain any vertices');
}
end($this->vertices);
return current($this->vertices);
}
/**
* return first Vertex that matches the given callback filter function
*
* @param callable $callbackCheck
* @return Vertex
* @throws UnderflowException if no Vertex matches the given callback filter function
* @uses self::getVertexMatchOrNull()
* @see self::getVerticesMatch() if you want to return *all* Vertices that match
*/
public function getVertexMatch($callbackCheck)
{
$ret = $this->getVertexMatchOrNull($callbackCheck);
if ($ret === null) {
throw new UnderflowException('No vertex found');
}
return $ret;
}
/**
* checks whether there's a Vertex that matches the given callback filter function
*
* @param callable $callbackCheck
* @return bool
* @see self::getVertexMatch() to return the Vertex instance that matches the given callback filter function
* @uses self::getVertexMatchOrNull()
*/
public function hasVertexMatch($callbackCheck)
{
return ($this->getVertexMatchOrNull($callbackCheck) !== null);
}
/**
* get a new set of Vertices that match the given callback filter function
*
* This only keeps Vertex elements if the $callbackCheck returns a bool
* true and filters out everything else.
*
* Vertex index positions will be left unchanged, so if you call this method
* on a VerticesMap, it will also return a VerticesMap.
*
* @param callable $callbackCheck
* @return Vertices a new Vertices instance
* @see self::getVertexMatch()
*/
public function getVerticesMatch($callbackCheck)
{
return new static(array_filter($this->vertices, $callbackCheck));
}
/**
* get new Set of Vertices ordered by given criterium $orderBy
*
* Vertex index positions will be left unchanged, so if you call this method
* on a VerticesMap, it will also return a VerticesMap.
*
* @param int $orderBy criterium to sort by. see Vertex::ORDER_ID, etc.
* @param bool $desc whether to return biggest first (true) instead of smallest first (default:false)
* @return Vertices a new Vertices set ordered by the given $orderBy criterium
* @throws InvalidArgumentException if criterium is unknown
* @see self::getVertexOrder()
*/
public function getVerticesOrder($orderBy, $desc = false)
{
if ($orderBy === self::ORDER_RANDOM) {
// shuffle the vertex positions
$keys = array_keys($this->vertices);
shuffle($keys);
// re-order according to shuffled vertex positions
$vertices = array();
foreach ($keys as $key) {
$vertices[$key] = $this->vertices[$key];
}
// create iterator for shuffled array (no need to check DESC flag)
return new static($vertices);
}
$callback = $this->getCallback($orderBy);
$array = $this->vertices;
uasort($array, function (Vertex $va, Vertex $vb) use ($callback, $desc) {
$ra = $callback($desc ? $vb : $va);
$rb = $callback($desc ? $va : $vb);
if ($ra < $rb) {
return -1;
} elseif ($ra > $rb) {
return 1;
} else {
return 0;
}
});
return new static($array);
}
/**
* get intersection of Vertices with given other Vertices
*
* The intersection contains all Vertex instances that are present in BOTH
* this set of Vertices and the given set of other Vertices.
*
* Vertex index/keys will be preserved from original array.
*
* Duplicate Vertex instances will be kept if the corresponding number of
* Vertex instances is also found in $otherVertices.
*
* @param Vertices|Vertex[] $otherVertices
* @return Vertices a new Vertices set
*/
public function getVerticesIntersection($otherVertices)
{
$otherArray = self::factory($otherVertices)->getVector();
$vertices = array();
foreach ($this->vertices as $vid => $vertex) {
$i = array_search($vertex, $otherArray, true);
if ($i !== false) {
// remove from other array in order to check for duplicate matches
unset($otherArray[$i]);
$vertices[$vid] = $vertex;
}
}
return new static($vertices);
}
/**
* get first vertex (optionally ordered by given criterium $by) from given array of vertices
*
* @param int $orderBy criterium to sort by. see Vertex::ORDER_ID, etc.
* @param bool $desc whether to return biggest (true) instead of smallest (default:false)
* @return Vertex
* @throws InvalidArgumentException if criterium is unknown
* @throws UnderflowException if no vertices exist
* @see self::getVerticesOrder()
*/
public function getVertexOrder($orderBy, $desc=false)
{
if (!$this->vertices) {
throw new UnderflowException('No vertex found');
}
// random order
if ($orderBy === self::ORDER_RANDOM) {
// just return by random key (no need to check for DESC flag)
return $this->vertices[array_rand($this->vertices)];
}
$callback = $this->getCallback($orderBy);
$ret = NULL;
$best = NULL;
foreach ($this->vertices as $vertex) {
$now = $callback($vertex);
if ($ret === NULL || ($desc && $now > $best) || (!$desc && $now < $best)) {
$ret = $vertex;
$best = $now;
}
}
return $ret;
}
/**
* return self reference to Set of Vertices
*
* @return Vertices
* @see self::factory()
*/
public function getVertices()
{
return $this;
}
/**
* get a new set of Vertices where each Vertex is distinct/unique
*
* @return VerticesMap a new VerticesMap instance
* @uses self::getMap()
*/
public function getVerticesDistinct()
{
return new VerticesMap($this->getMap());
}
/**
* get a mapping array of Vertex ID => Vertex instance and thus remove duplicate vertices
*
* @return Vertex[] Vertex ID => Vertex instance
* @uses Vertex::getId()
*/
public function getMap()
{
$vertices = array();
foreach ($this->vertices as $vertex) {
$vertices[$vertex->getId()] = $vertex;
}
return $vertices;
}
/**
* return array of Vertex IDs
*
* @return array
*/
public function getIds()
{
$ids = array();
foreach ($this->vertices as $vertex) {
$ids []= $vertex->getId();
}
return $ids;
}
/**
* return array of Vertex instances
*
* @return Vertex[]
*/
public function getVector()
{
return array_values($this->vertices);
}
/**
* count number of vertices
*
* @return int
* @see self::isEmpty()
*/
#[\ReturnTypeWillChange]
public function count()
{
return count($this->vertices);
}
/**
* check whether this Set of Vertices is empty
*
* A Set if empty if no single Vertex instance is added. This is faster
* than calling `count() === 0`.
*
* @return bool
*/
public function isEmpty()
{
return !$this->vertices;
}
/**
* check whether this set contains any duplicate vertex instances
*
* @return bool
* @uses self::getMap()
*/
public function hasDuplicates()
{
return (count($this->vertices) !== count($this->getMap()));
}
/**
* get Iterator
*
* This method implements the IteratorAggregate interface and allows this
* Set of Vertices to be used in foreach loops.
*
* @return \IteratorIterator
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
return new \IteratorIterator(new \ArrayIterator($this->vertices));
}
/**
* call given $callback on each Vertex and sum their results
*
* @param callable $callback
* @return number
* @throws InvalidArgumentException for invalid callbacks
* @uses self::getCallback()
*/
public function getSumCallback($callback)
{
$callback = $this->getCallback($callback);
// return array_sum(array_map($callback, $this->vertices));
$sum = 0;
foreach ($this->vertices as $vertex) {
$sum += $callback($vertex);
}
return $sum;
}
private function getCallbackId($id)
{
return function (Vertex $vertex) use ($id) {
return ($vertex->getId() == $id);
};
}
private function getVertexMatchOrNull($callbackCheck)
{
$callbackCheck = $this->getCallback($callbackCheck);
foreach ($this->vertices as $vertex) {
if ($callbackCheck($vertex)) {
return $vertex;
}
}
return null;
}
/**
* get callback/Closure to be called on Vertex instances for given callback identifier
*
* @param callable|int $callback
* @throws InvalidArgumentException
* @return callable
*/
private function getCallback($callback)
{
if (is_callable($callback)) {
if (is_array($callback)) {
$callback = function (Vertex $vertex) use ($callback) {
return call_user_func($callback, $vertex);
};
}
return $callback;
}
static $methods = array(
self::ORDER_ID => 'getId',
self::ORDER_GROUP => 'getGroup'
);
if (!is_int($callback) || !isset($methods[$callback])) {
throw new InvalidArgumentException('Invalid callback given');
}
$method = $methods[$callback];
return function (Vertex $vertex) use ($method) {
return $vertex->$method();
};
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Fhaculty\Graph\Set;
/**
* Basic interface for every class that provides access to its Set of Vertices
*/
interface VerticesAggregate
{
/**
* @return Vertices
*/
public function getVertices();
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Fhaculty\Graph\Set;
use Fhaculty\Graph\Exception\OutOfBoundsException;
use Fhaculty\Graph\Vertex;
/**
* A set of Vertices that are already stored in a vertex ID => Vertex instance mapping array
*
* Among others, using a mapped array significantly speeds up accessing vertices
* by ID. However, there's no way to store multiple vertices with the same ID
* (i.e. each Vertex ID has to be unique).
*/
class VerticesMap extends Vertices
{
public function getMap()
{
return $this->vertices;
}
public function getVertexId($id)
{
if (!isset($this->vertices[$id])) {
throw new OutOfBoundsException('Invalid vertex ID');
}
return $this->vertices[$id];
}
public function hasVertexId($id)
{
return isset($this->vertices[$id]);
}
public function getVerticesDistinct()
{
return $this;
}
public function getIds()
{
return array_keys($this->vertices);
}
public function getIndexVertex(Vertex $vertex)
{
$id = $vertex->getId();
if (!isset($this->vertices[$id]) || $this->vertices[$id] !== $vertex) {
throw new OutOfBoundsException();
}
return $id;
}
/**
*
* @return VerticesMap
*/
public function getVertices()
{
return $this;
}
public function hasDuplicates()
{
return false;
}
}

406
vendor/clue/graph/src/Vertex.php vendored Normal file
View File

@@ -0,0 +1,406 @@
<?php
namespace Fhaculty\Graph;
use Fhaculty\Graph\Attribute\AttributeAware;
use Fhaculty\Graph\Attribute\AttributeBagReference;
use Fhaculty\Graph\Edge\Base as Edge;
use Fhaculty\Graph\Edge\Directed as EdgeDirected;
use Fhaculty\Graph\Edge\Undirected as EdgeUndirected;
use Fhaculty\Graph\Exception\BadMethodCallException;
use Fhaculty\Graph\Exception\InvalidArgumentException;
use Fhaculty\Graph\Set\Edges;
use Fhaculty\Graph\Set\EdgesAggregate;
use Fhaculty\Graph\Set\Vertices;
class Vertex implements EdgesAggregate, AttributeAware
{
private $id;
/**
* @var Edge[]
*/
private $edges = array();
/**
* @var Graph
*/
private $graph;
/**
* vertex balance
*
* @var int|float|NULL
* @see Vertex::setBalance()
*/
private $balance;
/**
* group number
*
* @var int
* @see Vertex::setGroup()
*/
private $group = 0;
private $attributes = array();
/**
* Create a new Vertex
*
* @param Graph $graph graph to be added to
* @param string|int $id identifier used to uniquely identify this vertex in the graph
* @see Graph::createVertex() to create new vertices
*/
public function __construct(Graph $graph, $id)
{
if (!is_int($id) && !is_string($id)) {
throw new InvalidArgumentException('Vertex ID has to be of type integer or string');
}
$this->id = $id;
$this->graph = $graph;
$graph->addVertex($this);
}
/**
* get graph this vertex is attached to
*
* @return Graph
*/
public function getGraph()
{
return $this->graph;
}
public function getBalance()
{
return $this->balance;
}
public function setBalance($balance)
{
if ($balance !== NULL && !is_float($balance) && !is_int($balance)) {
throw new InvalidArgumentException('Invalid balance given - must be numeric');
}
$this->balance = $balance;
return $this;
}
/**
* set group number of this vertex
*
* @param int $group
* @return Vertex $this (chainable)
* @throws InvalidArgumentException if group is not numeric
*/
public function setGroup($group)
{
if (!is_int($group)) {
throw new InvalidArgumentException('Invalid group number');
}
$this->group = $group;
return $this;
}
/**
* get group number
*
* @return int
*/
public function getGroup()
{
return $this->group;
}
/**
* returns id of this Vertex
*
* @return int|string
*/
public function getId()
{
return $this->id;
}
/**
* create new directed edge from this start vertex to given target vertex
*
* @param Vertex $vertex target vertex
* @return EdgeDirected
* @throws InvalidArgumentException
* @uses Graph::addEdge()
*/
public function createEdgeTo(Vertex $vertex)
{
return new EdgeDirected($this, $vertex);
}
/**
* add new undirected (bidirectional) edge between this vertex and given vertex
*
* @param Vertex $vertex
* @return EdgeUndirected
* @throws InvalidArgumentException
* @uses Graph::addEdge()
*/
public function createEdge(Vertex $vertex)
{
return new EdgeUndirected($this, $vertex);
}
/**
* add the given edge to list of connected edges (MUST NOT be called manually)
*
* @param Edge $edge
* @return void
* @internal
* @see self::createEdge() instead!
*/
public function addEdge(Edge $edge)
{
$this->edges[] = $edge;
}
/**
* remove the given edge from list of connected edges (MUST NOT be called manually)
*
* @param Edge $edge
* @return void
* @throws InvalidArgumentException if given edge does not exist
* @internal
* @see Edge::destroy() instead!
*/
public function removeEdge(Edge $edge)
{
$id = array_search($edge, $this->edges, true);
if ($id === false) {
throw new InvalidArgumentException('Given edge does NOT exist');
}
unset($this->edges[$id]);
}
/**
* check whether this vertex has a direct edge to given $vertex
*
* @param Vertex $vertex
* @return bool
* @uses Edge::hasVertexTarget()
*/
public function hasEdgeTo(Vertex $vertex)
{
$that = $this;
return $this->getEdges()->hasEdgeMatch(function (Edge $edge) use ($that, $vertex) {
return $edge->isConnection($that, $vertex);
});
}
/**
* check whether the given vertex has a direct edge to THIS vertex
*
* @param Vertex $vertex
* @return bool
* @uses Vertex::hasEdgeTo()
*/
public function hasEdgeFrom(Vertex $vertex)
{
return $vertex->hasEdgeTo($this);
}
/**
* get set of ALL Edges attached to this vertex
*
* @return Edges
*/
public function getEdges()
{
return new Edges($this->edges);
}
/**
* get set of all outgoing Edges attached to this vertex
*
* @return Edges
*/
public function getEdgesOut()
{
$that = $this;
$prev = null;
return $this->getEdges()->getEdgesMatch(function (Edge $edge) use ($that, &$prev) {
$ret = $edge->hasVertexStart($that);
// skip duplicate directed loop edges
if ($edge === $prev && $edge instanceof EdgeDirected) {
$ret = false;
}
$prev = $edge;
return $ret;
});
}
/**
* get set of all ingoing Edges attached to this vertex
*
* @return Edges
*/
public function getEdgesIn()
{
$that = $this;
$prev = null;
return $this->getEdges()->getEdgesMatch(function (Edge $edge) use ($that, &$prev) {
$ret = $edge->hasVertexTarget($that);
// skip duplicate directed loop edges
if ($edge === $prev && $edge instanceof EdgeDirected) {
$ret = false;
}
$prev = $edge;
return $ret;
});
}
/**
* get set of Edges FROM this vertex TO the given vertex
*
* @param Vertex $vertex
* @return Edges
* @uses Edge::hasVertexTarget()
*/
public function getEdgesTo(Vertex $vertex)
{
$that = $this;
return $this->getEdges()->getEdgesMatch(function (Edge $edge) use ($that, $vertex) {
return $edge->isConnection($that, $vertex);
});
}
/**
* get set of Edges FROM the given vertex TO this vertex
*
* @param Vertex $vertex
* @return Edges
* @uses Vertex::getEdgesTo()
*/
public function getEdgesFrom(Vertex $vertex)
{
return $vertex->getEdgesTo($this);
}
/**
* get set of adjacent Vertices of this vertex (edge FROM or TO this vertex)
*
* If there are multiple parallel edges between the same Vertex, it will be
* returned several times in the resulting Set of Vertices. If you only
* want unique Vertex instances, use `getVerticesDistinct()`.
*
* @return Vertices
* @uses Edge::hasVertexStart()
* @uses Edge::getVerticesToFrom()
* @uses Edge::getVerticesFromTo()
*/
public function getVerticesEdge()
{
$ret = array();
foreach ($this->edges as $edge) {
if ($edge->hasVertexStart($this)) {
$ret []= $edge->getVertexToFrom($this);
} else {
$ret []= $edge->getVertexFromTo($this);
}
}
return new Vertices($ret);
}
/**
* get set of all Vertices this vertex has an edge to
*
* If there are multiple parallel edges to the same Vertex, it will be
* returned several times in the resulting Set of Vertices. If you only
* want unique Vertex instances, use `getVerticesDistinct()`.
*
* @return Vertices
* @uses Vertex::getEdgesOut()
* @uses Edge::getVerticesToFrom()
*/
public function getVerticesEdgeTo()
{
$ret = array();
foreach ($this->getEdgesOut() as $edge) {
$ret []= $edge->getVertexToFrom($this);
}
return new Vertices($ret);
}
/**
* get set of all Vertices that have an edge TO this vertex
*
* If there are multiple parallel edges from the same Vertex, it will be
* returned several times in the resulting Set of Vertices. If you only
* want unique Vertex instances, use `getVerticesDistinct()`.
*
* @return Vertices
* @uses Vertex::getEdgesIn()
* @uses Edge::getVerticesFromTo()
*/
public function getVerticesEdgeFrom()
{
$ret = array();
foreach ($this->getEdgesIn() as $edge) {
$ret []= $edge->getVertexFromTo($this);
}
return new Vertices($ret);
}
/**
* destroy vertex and all edges connected to it and remove reference from graph
*
* @uses Edge::destroy()
* @uses Graph::removeVertex()
*/
public function destroy()
{
foreach ($this->getEdges()->getEdgesDistinct() as $edge) {
$edge->destroy();
}
$this->graph->removeVertex($this);
}
/**
* do NOT allow cloning of objects
*
* @throws BadMethodCallException
*/
private function __clone()
{
// @codeCoverageIgnoreStart
throw new BadMethodCallException();
// @codeCoverageIgnoreEnd
}
public function getAttribute($name, $default = null)
{
return isset($this->attributes[$name]) ? $this->attributes[$name] : $default;
}
public function setAttribute($name, $value)
{
$this->attributes[$name] = $value;
}
public function getAttributeBag()
{
return new AttributeBagReference($this->attributes);
}
}

311
vendor/clue/graph/src/Walk.php vendored Normal file
View File

@@ -0,0 +1,311 @@
<?php
namespace Fhaculty\Graph;
use Fhaculty\Graph\Set\Edges;
use Fhaculty\Graph\Set\Vertices;
use Fhaculty\Graph\Edge\Base as Edge;
use Fhaculty\Graph\Exception\UnderflowException;
use Fhaculty\Graph\Exception\InvalidArgumentException;
use Fhaculty\Graph\Set\DualAggregate;
/**
* Base Walk class
*
* The general term "Walk" bundles the following mathematical concepts:
* walk, path, cycle, circuit, loop, trail, tour, etc.
*
* @link http://en.wikipedia.org/wiki/Path_%28graph_theory%29
* @link http://en.wikipedia.org/wiki/Glossary_of_graph_theory#Walks
* @see Fhaculty\Graph\Algorithm\Property\WalkProperty for checking special cases, such as cycles, loops, closed trails, etc.
*/
class Walk implements DualAggregate
{
/**
* construct new walk from given start vertex and given array of edges
*
* @param Edges|Edge[] $edges
* @param Vertex $startVertex
* @return Walk
*/
public static function factoryFromEdges($edges, Vertex $startVertex)
{
$vertices = array($startVertex);
$vertexCurrent = $startVertex;
foreach ($edges as $edge) {
$vertexCurrent = $edge->getVertexToFrom($vertexCurrent);
$vertices []= $vertexCurrent;
}
return new self($vertices, $edges);
}
/**
* create new walk instance between given set of Vertices / array of Vertex instances
*
* @param Vertices|Vertex[] $vertices
* @param int|null $by
* @param bool $desc
* @return Walk
* @throws UnderflowException if no vertices were given
* @see Edges::getEdgeOrder() for parameters $by and $desc
*/
public static function factoryFromVertices($vertices, $by = null, $desc = false)
{
$edges = array();
$last = NULL;
foreach ($vertices as $vertex) {
// skip first vertex as last is unknown
if ($last !== NULL) {
// pick edge between last vertex and this vertex
/* @var $last Vertex */
if ($by === null) {
$edges []= $last->getEdgesTo($vertex)->getEdgeFirst();
} else {
$edges []= $last->getEdgesTo($vertex)->getEdgeOrder($by, $desc);
}
}
$last = $vertex;
}
if ($last === NULL) {
throw new UnderflowException('No vertices given');
}
return new self($vertices, $edges);
}
/**
* create new cycle instance from given predecessor map
*
* @param Vertex[] $predecessors map of vid => predecessor vertex instance
* @param Vertex $vertex start vertex to search predecessors from
* @param int|null $by
* @param bool $desc
* @return Walk
* @throws UnderflowException
* @see Edges::getEdgeOrder() for parameters $by and $desc
* @uses self::factoryFromVertices()
*/
public static function factoryCycleFromPredecessorMap(array $predecessors, Vertex $vertex, $by = null, $desc = false)
{
// find a vertex in the cycle
$vid = $vertex->getId();
$startVertices = array();
do {
if (!isset($predecessors[$vid])) {
throw new InvalidArgumentException('Predecessor map is incomplete and does not form a cycle');
}
$startVertices[$vid] = $vertex;
$vertex = $predecessors[$vid];
$vid = $vertex->getId();
} while (!isset($startVertices[$vid]));
// find negative cycle
$vid = $vertex->getId();
// build array of vertices in cycle
$vertices = array();
do {
// add new vertex to cycle
$vertices[$vid] = $vertex;
// get predecessor of vertex
$vertex = $predecessors[$vid];
$vid = $vertex->getId();
// continue until we find a vertex that's already in the circle (i.e. circle is closed)
} while (!isset($vertices[$vid]));
// reverse cycle, because cycle is actually built in opposite direction due to checking predecessors
$vertices = array_reverse($vertices, true);
// additional edge from last vertex to first vertex
$vertices[] = reset($vertices);
return self::factoryCycleFromVertices($vertices, $by, $desc);
}
/**
* create new cycle instance with edges between given vertices
*
* @param Vertex[]|Vertices $vertices
* @param int|null $by
* @param bool $desc
* @return Walk
* @throws UnderflowException if no vertices were given
* @see Edges::getEdgeOrder() for parameters $by and $desc
* @uses self::factoryFromVertices()
*/
public static function factoryCycleFromVertices($vertices, $by = null, $desc = false)
{
$cycle = self::factoryFromVertices($vertices, $by, $desc);
if ($cycle->getEdges()->isEmpty()) {
throw new InvalidArgumentException('Cycle with no edges can not exist');
}
if ($cycle->getVertices()->getVertexFirst() !== $cycle->getVertices()->getVertexLast()) {
throw new InvalidArgumentException('Cycle has to start and end at the same vertex');
}
return $cycle;
}
/**
* create new cycle instance with vertices connected by given edges
*
* @param Edges|Edge[] $edges
* @param Vertex $startVertex
* @return Walk
* @throws InvalidArgumentException if the given array of edges does not represent a valid cycle
* @uses self::factoryFromEdges()
*/
public static function factoryCycleFromEdges($edges, Vertex $startVertex)
{
$cycle = self::factoryFromEdges($edges, $startVertex);
// ensure this walk is actually a cycle by checking start = end
if ($cycle->getVertices()->getVertexLast() !== $startVertex) {
throw new InvalidArgumentException('The given array of edges does not represent a cycle');
}
return $cycle;
}
/**
*
* @var Vertices
*/
protected $vertices;
/**
*
* @var Edges
*/
protected $edges;
protected function __construct($vertices, $edges)
{
$this->vertices = Vertices::factory($vertices);
$this->edges = Edges::factory($edges);
}
/**
* return original graph
*
* @return Graph
* @uses self::getVertices()
* @uses Vertices::getVertexFirst()
* @uses Vertex::getGraph()
*/
public function getGraph()
{
return $this->getVertices()->getVertexFirst()->getGraph();
}
/**
* create new graph clone with only vertices and edges actually in the walk
*
* do not add duplicate vertices and edges for loops and intersections, etc.
*
* @return Graph
* @uses Walk::getEdges()
* @uses Graph::createGraphCloneEdges()
*/
public function createGraph()
{
// create new graph clone with only edges of walk
$graph = $this->getGraph()->createGraphCloneEdges($this->getEdges());
$vertices = $this->getVertices()->getMap();
// get all vertices
foreach ($graph->getVertices()->getMap() as $vid => $vertex) {
if (!isset($vertices[$vid])) {
// remove those not present in the walk (isolated vertices, etc.)
$vertex->destroy();
}
}
return $graph;
}
/**
* return set of all Edges of walk (in sequence visited in walk, may contain duplicates)
*
* If you need to return set a of all unique Edges of walk, use
* `Walk::getEdges()->getEdgesDistinct()` instead.
*
* @return Edges
*/
public function getEdges()
{
return $this->edges;
}
/**
* return set of all Vertices of walk (in sequence visited in walk, may contain duplicates)
*
* If you need to return set a of all unique Vertices of walk, use
* `Walk::getVertices()->getVerticesDistinct()` instead.
*
* If you need to return the source vertex (first vertex of walk), use
* `Walk::getVertices()->getVertexFirst()` instead.
*
* If you need to return the target/destination vertex (last vertex of walk), use
* `Walk::getVertices()->getVertexLast()` instead.
*
* @return Vertices
*/
public function getVertices()
{
return $this->vertices;
}
/**
* get alternating sequence of vertex, edge, vertex, edge, ..., vertex
*
* @return array
*/
public function getAlternatingSequence()
{
$edges = $this->edges->getVector();
$vertices = $this->vertices->getVector();
$ret = array();
for ($i = 0, $l = count($this->edges); $i < $l; ++$i) {
$ret []= $vertices[$i];
$ret []= $edges[$i];
}
$ret[] = $vertices[$i];
return $ret;
}
/**
* check to make sure this walk is still valid (i.e. source graph still contains all vertices and edges)
*
* @return bool
* @uses Walk::getGraph()
* @uses Graph::getVertices()
* @uses Graph::getEdges()
*/
public function isValid()
{
$vertices = $this->getGraph()->getVertices()->getMap();
// check source graph contains all vertices
foreach ($this->getVertices()->getMap() as $vid => $vertex) {
// make sure vertex ID exists and has not been replaced
if (!isset($vertices[$vid]) || $vertices[$vid] !== $vertex) {
return false;
}
}
$edges = $this->getGraph()->getEdges()->getVector();
// check source graph contains all edges
foreach ($this->edges as $edge) {
if (!in_array($edge, $edges, true)) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,2 @@
github: clue
custom: https://clue.engineering/support

97
vendor/clue/stream-filter/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,97 @@
# Changelog
## 1.7.0 (2023-12-20)
* Feature: Full PHP 8.3 and PHP 8.2 compatibility.
(#51 by @yadaiio and #50 by @clue)
* Feature / Fix: Improve error reporting when custom error handler is used.
(#47 by @SimonFrings)
* Improve test suite and ensure 100% code coverage.
(#46 by @SimonFrings, #48 and #50 by @clue and #51 by @yadaiio)
## 1.6.0 (2022-02-21)
* Feature: Support PHP 8.1 release.
(#45 by @clue)
* Improve documentation to use fully-qualified function names.
(#43 by @SimonFrings and #42 by @PaulRotmann)
* Improve test suite and use GitHub actions for continuous integration (CI).
(#39 and #40 by @SimonFrings)
## 1.5.0 (2020-10-02)
* Feature: Improve performance by using global imports.
(#38 by @clue)
* Improve API documentation and add support / sponsorship info.
(#30 by @clue and #35 by @SimonFrings)
* Improve test suite and add `.gitattributes` to exclude dev files from exports.
Prepare PHP 8 support, update to PHPUnit 9 and simplify test matrix.
(#32 and #37 by @clue and #34 and #36 by @SimonFrings)
## 1.4.1 (2019-04-09)
* Fix: Check if the function is declared before declaring it.
(#23 by @Niko9911)
* Improve test suite to also test against PHP 7.2 and
add test for base64 encoding and decoding filters.
(#22 by @arubacao and #25 by @Nyholm and @clue)
## 1.4.0 (2017-08-18)
* Feature / Fix: The `fun()` function does not pass filter parameter `null`
to underlying `stream_filter_append()` by default
(#15 by @Nyholm)
Certain filters (such as `convert.quoted-printable-encode`) do not accept
a filter parameter at all. If no explicit filter parameter is given, we no
longer pass a default `null` value.
```php
$encode = Filter\fun('convert.quoted-printable-encode');
assert('t=C3=A4st' === $encode('täst'));
```
* Add examples and improve documentation
(#13 and #20 by @clue and #18 by @Nyholm)
* Improve test suite by adding PHPUnit to require-dev,
fix HHVM build for now again and ignore future HHVM build errors,
lock Travis distro so new future defaults will not break the build
and test on PHP 7.1
(#12, #14 and #19 by @clue and #16 by @Nyholm)
## 1.3.0 (2015-11-08)
* Feature: Support accessing built-in filters as callbacks
(#5 by @clue)
```php
$fun = Filter\fun('zlib.deflate');
$ret = $fun('hello') . $fun('world') . $fun();
assert('helloworld' === gzinflate($ret));
```
## 1.2.0 (2015-10-23)
* Feature: Invoke close event when closing filter (flush buffer)
(#9 by @clue)
## 1.1.0 (2015-10-22)
* Feature: Abort filter operation when catching an Exception
(#10 by @clue)
* Feature: Additional safeguards to prevent filter state corruption
(#7 by @clue)
## 1.0.0 (2015-10-18)
* First tagged release

21
vendor/clue/stream-filter/LICENSE vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Christian Lück
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

326
vendor/clue/stream-filter/README.md vendored Normal file
View File

@@ -0,0 +1,326 @@
# clue/stream-filter
[![CI status](https://github.com/clue/stream-filter/actions/workflows/ci.yml/badge.svg)](https://github.com/clue/stream-filter/actions)
[![installs on Packagist](https://img.shields.io/packagist/dt/clue/stream-filter?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/stream-filter)
A simple and modern approach to stream filtering in PHP
**Table of contents**
* [Why?](#why)
* [Support us](#support-us)
* [Usage](#usage)
* [append()](#append)
* [prepend()](#prepend)
* [fun()](#fun)
* [remove()](#remove)
* [Install](#install)
* [Tests](#tests)
* [License](#license)
## Why?
PHP's stream filtering system is great!
It offers very powerful stream filtering options and comes with a useful set of built-in filters.
These filters can be used to easily and efficiently perform various transformations on-the-fly, such as:
* read from a gzip'ed input file,
* transcode from ISO-8859-1 (Latin1) to UTF-8,
* write to a bzip output file
* and much more.
But let's face it:
Its API is [*difficult to work with*](https://www.php.net/manual/en/php-user-filter.filter.php)
and its documentation is [*subpar*](https://stackoverflow.com/questions/27103269/what-is-a-bucket-brigade).
This combined means its powerful features are often neglected.
This project aims to make these features more accessible to a broader audience.
* **Lightweight, SOLID design** -
Provides a thin abstraction that is [*just good enough*](https://en.wikipedia.org/wiki/Principle_of_good_enough)
and does not get in your way.
Custom filters require trivial effort.
* **Good test coverage** -
Comes with an automated tests suite and is regularly tested in the *real world*
## Support us
We invest a lot of time developing, maintaining and updating our awesome
open-source projects. You can help us sustain this high-quality of our work by
[becoming a sponsor on GitHub](https://github.com/sponsors/clue). Sponsors get
numerous benefits in return, see our [sponsoring page](https://github.com/sponsors/clue)
for details.
Let's take these projects to the next level together! 🚀
## Usage
This lightweight library consists only of a few simple functions.
All functions reside under the `Clue\StreamFilter` namespace.
The below examples refer to all functions with their fully-qualified names like this:
```php
Clue\StreamFilter\append();
```
As of PHP 5.6+ you can also import each required function into your code like this:
```php
use function Clue\StreamFilter\append;
append();
```
Alternatively, you can also use an import statement similar to this:
```php
use Clue\StreamFilter as Filter;
Filter\append();
```
### append()
The `append(resource<stream> $stream, callable $callback, int $read_write = STREAM_FILTER_ALL): resource<stream filter>` function can be used to
append a filter callback to the given stream.
Each stream can have a list of filters attached.
This function appends a filter to the end of this list.
If the given filter can not be added, it throws an `Exception`.
The `$stream` can be any valid stream resource, such as:
```php
$stream = fopen('demo.txt', 'w+');
```
The `$callback` should be a valid callable function which accepts
an individual chunk of data and should return the updated chunk:
```php
$filter = Clue\StreamFilter\append($stream, function ($chunk) {
// will be called each time you read or write a $chunk to/from the stream
return $chunk;
});
```
As such, you can also use native PHP functions or any other `callable`:
```php
Clue\StreamFilter\append($stream, 'strtoupper');
// will write "HELLO" to the underlying stream
fwrite($stream, 'hello');
```
If the `$callback` accepts invocation without parameters,
then this signature will be invoked once ending (flushing) the filter:
```php
Clue\StreamFilter\append($stream, function ($chunk = null) {
if ($chunk === null) {
// will be called once ending the filter
return 'end';
}
// will be called each time you read or write a $chunk to/from the stream
return $chunk;
});
fclose($stream);
```
> Note: Legacy PHP versions (PHP < 5.4) do not support passing additional data
from the end signal handler if the stream is being closed.
If your callback throws an `Exception`, then the filter process will be aborted.
In order to play nice with PHP's stream handling,
the `Exception` will be transformed to a PHP warning instead:
```php
Clue\StreamFilter\append($stream, function ($chunk) {
throw new \RuntimeException('Unexpected chunk');
});
// raises an E_USER_WARNING with "Error invoking filter: Unexpected chunk"
fwrite($stream, 'hello');
```
The optional `$read_write` parameter can be used to only invoke the `$callback`
when either writing to the stream or only when reading from the stream:
```php
Clue\StreamFilter\append($stream, function ($chunk) {
// will be called each time you write to the stream
return $chunk;
}, STREAM_FILTER_WRITE);
Clue\StreamFilter\append($stream, function ($chunk) {
// will be called each time you read from the stream
return $chunk;
}, STREAM_FILTER_READ);
```
This function returns a filter resource which can be passed to [`remove()`](#remove).
> Note that once a filter has been added to stream, the stream can no longer be passed to
> [`stream_select()`](https://www.php.net/manual/en/function.stream-select.php)
> (and family).
>
> > Warning: stream_select(): cannot cast a filtered stream on this system in {file} on line {line}
>
> This is due to limitations of PHP's stream filter support, as it can no longer reliably
> tell when the underlying stream resource is actually ready.
> As an alternative, consider calling `stream_select()` on the unfiltered stream and
> then pass the unfiltered data through the [`fun()`](#fun) function.
### prepend()
The `prepend(resource<stream> $stream, callable $callback, int $read_write = STREAM_FILTER_ALL): resource<stream filter>` function can be used to
prepend a filter callback to the given stream.
Each stream can have a list of filters attached.
This function prepends a filter to the start of this list.
If the given filter can not be added, it throws an `Exception`.
```php
$filter = Clue\StreamFilter\prepend($stream, function ($chunk) {
// will be called each time you read or write a $chunk to/from the stream
return $chunk;
});
```
This function returns a filter resource which can be passed to [`remove()`](#remove).
Except for the position in the list of filters, this function behaves exactly
like the [`append()`](#append) function.
For more details about its behavior, see also the [`append()`](#append) function.
### fun()
The `fun(string $filter, mixed $parameters = null): callable` function can be used to
create a filter function which uses the given built-in `$filter`.
PHP comes with a useful set of [built-in filters](https://www.php.net/manual/en/filters.php).
Using `fun()` makes accessing these as easy as passing an input string to filter
and getting the filtered output string.
```php
$fun = Clue\StreamFilter\fun('string.rot13');
assert('grfg' === $fun('test'));
assert('test' === $fun($fun('test'));
```
Please note that not all filter functions may be available depending
on installed PHP extensions and the PHP version in use.
In particular, [HHVM](https://hhvm.com/) may not offer the same filter functions
or parameters as Zend PHP.
Accessing an unknown filter function will result in a `RuntimeException`:
```php
Clue\StreamFilter\fun('unknown'); // throws RuntimeException
```
Some filters may accept or require additional filter parameters most
filters do not require filter parameters.
If given, the optional `$parameters` argument will be passed to the
underlying filter handler as-is.
In particular, note how *not passing* this parameter at all differs from
explicitly passing a `null` value (which many filters do not accept).
Please refer to the individual filter definition for more details.
For example, the `string.strip_tags` filter can be invoked like this:
```php
$fun = Clue\StreamFilter\fun('string.strip_tags', '<a><b>');
$ret = $fun('<b>h<br>i</b>');
assert('<b>hi</b>' === $ret);
```
Under the hood, this function allocates a temporary memory stream, so it's
recommended to clean up the filter function after use.
Also, some filter functions (in particular the
[zlib compression filters](https://www.php.net/manual/en/filters.compression.php))
may use internal buffers and may emit a final data chunk on close.
The filter function can be closed by invoking without any arguments:
```php
$fun = Clue\StreamFilter\fun('zlib.deflate');
$ret = $fun('hello') . $fun('world') . $fun();
assert('helloworld' === gzinflate($ret));
```
The filter function must not be used anymore after it has been closed.
Doing so will result in a `RuntimeException`:
```php
$fun = Clue\StreamFilter\fun('string.rot13');
$fun();
$fun('test'); // throws RuntimeException
```
> Note: If you're using the zlib compression filters, then you should be wary
about engine inconsistencies between different PHP versions and HHVM.
These inconsistencies exist in the underlying PHP engines and there's little we
can do about this in this library.
[Our test suite](tests/) contains several test cases that exhibit these issues.
If you feel some test case is missing or outdated, we're happy to accept PRs! :)
### remove()
The `remove(resource<stream filter> $filter): bool` function can be used to
remove a filter previously added via [`append()`](#append) or [`prepend()`](#prepend).
```php
$filter = Clue\StreamFilter\append($stream, function () {
// …
});
Clue\StreamFilter\remove($filter);
```
## Install
The recommended way to install this library is [through Composer](https://getcomposer.org/).
[New to Composer?](https://getcomposer.org/doc/00-intro.md)
This project follows [SemVer](https://semver.org/).
This will install the latest supported version:
```bash
$ composer require clue/stream-filter:^1.7
```
See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
This project aims to run on any platform and thus does not require any PHP
extensions and supports running on legacy PHP 5.3 through current PHP 8+ and
HHVM.
It's *highly recommended to use the latest supported PHP version* for this project.
Older PHP versions may suffer from a number of inconsistencies documented above.
## Tests
To run the test suite, you first need to clone this repo and then install all
dependencies [through Composer](https://getcomposer.org/):
```bash
$ composer install
```
To run the test suite, go to the project root and run:
```bash
$ vendor/bin/phpunit
```
## License
This project is released under the permissive [MIT license](LICENSE).
> Did you know that I offer custom development services and issuing invoices for
sponsorships of releases and for contributions? Contact me (@clue) for details.

32
vendor/clue/stream-filter/composer.json vendored Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "clue/stream-filter",
"description": "A simple and modern approach to stream filtering in PHP",
"keywords": ["stream", "callback", "filter", "php_user_filter", "stream_filter_append", "stream_filter_register", "bucket brigade"],
"homepage": "https://github.com/clue/stream-filter",
"license": "MIT",
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering"
}
],
"require": {
"php": ">=5.3"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
},
"autoload": {
"psr-4": {
"Clue\\StreamFilter\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"autoload-dev": {
"psr-4": {
"Clue\\Tests\\StreamFilter\\": "tests/"
}
}
}

View File

@@ -0,0 +1,120 @@
<?php
namespace Clue\StreamFilter;
/**
* @internal
* @see append()
* @see prepend()
*/
class CallbackFilter extends \php_user_filter
{
private $callback;
private $closed = true;
private $supportsClose = false;
/** @return bool */
#[\ReturnTypeWillChange]
public function onCreate()
{
$this->closed = false;
if (!\is_callable($this->params)) {
throw new \InvalidArgumentException('No valid callback parameter given to stream_filter_(append|prepend)');
}
$this->callback = $this->params;
// callback supports end event if it accepts invocation without arguments
$ref = new \ReflectionFunction($this->callback);
$this->supportsClose = ($ref->getNumberOfRequiredParameters() === 0);
return true;
}
/** @return void */
#[\ReturnTypeWillChange]
public function onClose()
{
$this->closed = true;
// callback supports closing and is not already closed
if ($this->supportsClose) {
$this->supportsClose = false;
// invoke without argument to signal end and discard resulting buffer
try {
\call_user_func($this->callback);
} catch (\Exception $ignored) {
// this might be called during engine shutdown, so it's not safe
// to raise any errors or exceptions here
// trigger_error('Error closing filter: ' . $ignored->getMessage(), E_USER_WARNING);
}
}
$this->callback = null;
}
/** @return int */
#[\ReturnTypeWillChange]
public function filter($in, $out, &$consumed, $closing)
{
// concatenate whole buffer from input brigade
$data = '';
while ($bucket = \stream_bucket_make_writeable($in)) {
$consumed += $bucket->datalen;
$data .= $bucket->data;
}
// skip processing callback that already ended
if ($this->closed) {
return \PSFS_FEED_ME;
}
// only invoke filter function if buffer is not empty
// this may skip flushing a closing filter
if ($data !== '') {
try {
$data = \call_user_func($this->callback, $data);
} catch (\Exception $e) {
// exception should mark filter as closed
$this->onClose();
\trigger_error('Error invoking filter: ' . $e->getMessage(), \E_USER_WARNING);
return \PSFS_ERR_FATAL;
}
}
// mark filter as closed after processing closing chunk
if ($closing) {
$this->closed = true;
// callback supports closing and is not already closed
if ($this->supportsClose) {
$this->supportsClose = false;
// invoke without argument to signal end and append resulting buffer
try {
$data .= \call_user_func($this->callback);
} catch (\Exception $e) {
\trigger_error('Error ending filter: ' . $e->getMessage(), \E_USER_WARNING);
return \PSFS_ERR_FATAL;
}
}
}
if ($data !== '') {
// create a new bucket for writing the resulting buffer to the output brigade
// reusing an existing bucket turned out to be bugged in some environments (ancient PHP versions and HHVM)
$bucket = @\stream_bucket_new($this->stream, $data);
// legacy PHP versions (PHP < 5.4) do not support passing data from the event signal handler
// because closing the stream invalidates the stream and its stream bucket brigade before
// invoking the filter close handler.
if ($bucket !== false) {
\stream_bucket_append($out, $bucket);
}
}
return \PSFS_PASS_ON;
}
}

View File

@@ -0,0 +1,380 @@
<?php
namespace Clue\StreamFilter;
/**
* Append a filter callback to the given stream.
*
* Each stream can have a list of filters attached.
* This function appends a filter to the end of this list.
*
* If the given filter can not be added, it throws an `Exception`.
*
* The `$stream` can be any valid stream resource, such as:
*
* ```php
* $stream = fopen('demo.txt', 'w+');
* ```
*
* The `$callback` should be a valid callable function which accepts
* an individual chunk of data and should return the updated chunk:
*
* ```php
* $filter = Clue\StreamFilter\append($stream, function ($chunk) {
* // will be called each time you read or write a $chunk to/from the stream
* return $chunk;
* });
* ```
*
* As such, you can also use native PHP functions or any other `callable`:
*
* ```php
* Clue\StreamFilter\append($stream, 'strtoupper');
*
* // will write "HELLO" to the underlying stream
* fwrite($stream, 'hello');
* ```
*
* If the `$callback` accepts invocation without parameters,
* then this signature will be invoked once ending (flushing) the filter:
*
* ```php
* Clue\StreamFilter\append($stream, function ($chunk = null) {
* if ($chunk === null) {
* // will be called once ending the filter
* return 'end';
* }
* // will be called each time you read or write a $chunk to/from the stream
* return $chunk;
* });
*
* fclose($stream);
* ```
*
* > Note: Legacy PHP versions (PHP < 5.4) do not support passing additional data
* from the end signal handler if the stream is being closed.
*
* If your callback throws an `Exception`, then the filter process will be aborted.
* In order to play nice with PHP's stream handling,
* the `Exception` will be transformed to a PHP warning instead:
*
* ```php
* Clue\StreamFilter\append($stream, function ($chunk) {
* throw new \RuntimeException('Unexpected chunk');
* });
*
* // raises an E_USER_WARNING with "Error invoking filter: Unexpected chunk"
* fwrite($stream, 'hello');
* ```
*
* The optional `$read_write` parameter can be used to only invoke the `$callback`
* when either writing to the stream or only when reading from the stream:
*
* ```php
* Clue\StreamFilter\append($stream, function ($chunk) {
* // will be called each time you write to the stream
* return $chunk;
* }, STREAM_FILTER_WRITE);
*
* Clue\StreamFilter\append($stream, function ($chunk) {
* // will be called each time you read from the stream
* return $chunk;
* }, STREAM_FILTER_READ);
* ```
*
* This function returns a filter resource which can be passed to [`remove()`](#remove).
*
* > Note that once a filter has been added to stream, the stream can no longer be passed to
* > [`stream_select()`](https://www.php.net/manual/en/function.stream-select.php)
* > (and family).
* >
* > > Warning: stream_select(): cannot cast a filtered stream on this system in {file} on line {line}
* >
* > This is due to limitations of PHP's stream filter support, as it can no longer reliably
* > tell when the underlying stream resource is actually ready.
* > As an alternative, consider calling `stream_select()` on the unfiltered stream and
* > then pass the unfiltered data through the [`fun()`](#fun) function.
*
* @param resource $stream
* @param callable $callback
* @param int $read_write
* @return resource filter resource which can be used for `remove()`
* @throws \Exception on error
* @uses stream_filter_append()
*/
function append($stream, $callback, $read_write = STREAM_FILTER_ALL)
{
$errstr = '';
\set_error_handler(function ($_, $error) use (&$errstr) {
// Match errstr from PHP's warning message.
// stream_filter_append() expects parameter 1 to be resource,...
$errstr = $error; // @codeCoverageIgnore
});
try {
$ret = \stream_filter_append($stream, register(), $read_write, $callback);
} catch (\TypeError $e) { // @codeCoverageIgnoreStart
// Throws TypeError on PHP 8.0+
\restore_error_handler();
throw $e;
} // @codeCoverageIgnoreEnd
\restore_error_handler();
// PHP 8 throws above on type errors, older PHP and memory issues can throw here
// @codeCoverageIgnoreStart
if ($ret === false) {
throw new \RuntimeException('Unable to append filter: ' . $errstr);
}
// @codeCoverageIgnoreEnd
return $ret;
}
/**
* Prepend a filter callback to the given stream.
*
* Each stream can have a list of filters attached.
* This function prepends a filter to the start of this list.
*
* If the given filter can not be added, it throws an `Exception`.
*
* ```php
* $filter = Clue\StreamFilter\prepend($stream, function ($chunk) {
* // will be called each time you read or write a $chunk to/from the stream
* return $chunk;
* });
* ```
*
* This function returns a filter resource which can be passed to [`remove()`](#remove).
*
* Except for the position in the list of filters, this function behaves exactly
* like the [`append()`](#append) function.
* For more details about its behavior, see also the [`append()`](#append) function.
*
* @param resource $stream
* @param callable $callback
* @param int $read_write
* @return resource filter resource which can be used for `remove()`
* @throws \Exception on error
* @uses stream_filter_prepend()
*/
function prepend($stream, $callback, $read_write = STREAM_FILTER_ALL)
{
$errstr = '';
\set_error_handler(function ($_, $error) use (&$errstr) {
// Match errstr from PHP's warning message.
// stream_filter_prepend() expects parameter 1 to be resource,...
$errstr = $error; // @codeCoverageIgnore
});
try {
$ret = \stream_filter_prepend($stream, register(), $read_write, $callback);
} catch (\TypeError $e) { // @codeCoverageIgnoreStart
// Throws TypeError on PHP 8.0+
\restore_error_handler();
throw $e;
} // @codeCoverageIgnoreEnd
\restore_error_handler();
// PHP 8 throws above on type errors, older PHP and memory issues can throw here
// @codeCoverageIgnoreStart
if ($ret === false) {
throw new \RuntimeException('Unable to prepend filter: ' . $errstr);
}
// @codeCoverageIgnoreEnd
return $ret;
}
/**
* Create a filter function which uses the given built-in `$filter`.
*
* PHP comes with a useful set of [built-in filters](https://www.php.net/manual/en/filters.php).
* Using `fun()` makes accessing these as easy as passing an input string to filter
* and getting the filtered output string.
*
* ```php
* $fun = Clue\StreamFilter\fun('string.rot13');
*
* assert('grfg' === $fun('test'));
* assert('test' === $fun($fun('test'));
* ```
*
* Please note that not all filter functions may be available depending
* on installed PHP extensions and the PHP version in use.
* In particular, [HHVM](https://hhvm.com/) may not offer the same filter functions
* or parameters as Zend PHP.
* Accessing an unknown filter function will result in a `RuntimeException`:
*
* ```php
* Clue\StreamFilter\fun('unknown'); // throws RuntimeException
* ```
*
* Some filters may accept or require additional filter parameters most
* filters do not require filter parameters.
* If given, the optional `$parameters` argument will be passed to the
* underlying filter handler as-is.
* In particular, note how *not passing* this parameter at all differs from
* explicitly passing a `null` value (which many filters do not accept).
* Please refer to the individual filter definition for more details.
* For example, the `string.strip_tags` filter can be invoked like this:
*
* ```php
* $fun = Clue\StreamFilter\fun('string.strip_tags', '<a><b>');
*
* $ret = $fun('<b>h<br>i</b>');
* assert('<b>hi</b>' === $ret);
* ```
*
* Under the hood, this function allocates a temporary memory stream, so it's
* recommended to clean up the filter function after use.
* Also, some filter functions (in particular the
* [zlib compression filters](https://www.php.net/manual/en/filters.compression.php))
* may use internal buffers and may emit a final data chunk on close.
* The filter function can be closed by invoking without any arguments:
*
* ```php
* $fun = Clue\StreamFilter\fun('zlib.deflate');
*
* $ret = $fun('hello') . $fun('world') . $fun();
* assert('helloworld' === gzinflate($ret));
* ```
*
* The filter function must not be used anymore after it has been closed.
* Doing so will result in a `RuntimeException`:
*
* ```php
* $fun = Clue\StreamFilter\fun('string.rot13');
* $fun();
*
* $fun('test'); // throws RuntimeException
* ```
*
* > Note: If you're using the zlib compression filters, then you should be wary
* about engine inconsistencies between different PHP versions and HHVM.
* These inconsistencies exist in the underlying PHP engines and there's little we
* can do about this in this library.
* [Our test suite](tests/) contains several test cases that exhibit these issues.
* If you feel some test case is missing or outdated, we're happy to accept PRs! :)
*
* @param string $filter built-in filter name. See stream_get_filters() or http://php.net/manual/en/filters.php
* @param mixed $parameters (optional) parameters to pass to the built-in filter as-is
* @return callable a filter callback which can be append()'ed or prepend()'ed
* @throws \RuntimeException on error
* @link http://php.net/manual/en/filters.php
* @see stream_get_filters()
* @see append()
*/
function fun($filter, $parameters = null)
{
$fp = \fopen('php://memory', 'w');
$errstr = '';
\set_error_handler(function ($_, $error) use (&$errstr) {
// Match errstr from PHP's warning message.
// stream_filter_append() expects parameter 1 to be resource,...
$errstr = $error;
});
if (\func_num_args() === 1) {
$filter = \stream_filter_append($fp, $filter, \STREAM_FILTER_WRITE);
} else {
$filter = \stream_filter_append($fp, $filter, \STREAM_FILTER_WRITE, $parameters);
}
\restore_error_handler();
if ($filter === false) {
\fclose($fp);
throw new \RuntimeException('Unable to access built-in filter: ' . $errstr);
}
// append filter function which buffers internally
$buffer = '';
append($fp, function ($chunk) use (&$buffer) {
$buffer .= $chunk;
// always return empty string in order to skip actually writing to stream resource
return '';
}, \STREAM_FILTER_WRITE);
$closed = false;
return function ($chunk = null) use ($fp, $filter, &$buffer, &$closed) {
if ($closed) {
throw new \RuntimeException('Unable to perform operation on closed stream');
}
if ($chunk === null) {
$closed = true;
$buffer = '';
\fclose($fp);
return $buffer;
}
// initialize buffer and invoke filters by attempting to write to stream
$buffer = '';
\fwrite($fp, $chunk);
// buffer now contains everything the filter function returned
return $buffer;
};
}
/**
* Remove a filter previously added via `append()` or `prepend()`.
*
* ```php
* $filter = Clue\StreamFilter\append($stream, function () {
* // …
* });
* Clue\StreamFilter\remove($filter);
* ```
*
* @param resource $filter
* @return bool true on success or false on error
* @throws \RuntimeException on error
* @uses stream_filter_remove()
*/
function remove($filter)
{
$errstr = '';
\set_error_handler(function ($_, $error) use (&$errstr) {
// Match errstr from PHP's warning message.
// stream_filter_remove() expects parameter 1 to be resource,...
$errstr = $error;
});
try {
$ret = \stream_filter_remove($filter);
} catch (\TypeError $e) { // @codeCoverageIgnoreStart
// Throws TypeError on PHP 8.0+
\restore_error_handler();
throw $e;
} // @codeCoverageIgnoreEnd
\restore_error_handler();
if ($ret === false) {
// PHP 8 throws above on type errors, older PHP and memory issues can throw here
throw new \RuntimeException('Unable to remove filter: ' . $errstr);
}
}
/**
* Registers the callback filter and returns the resulting filter name
*
* There should be little reason to call this function manually.
*
* @return string filter name
* @uses CallbackFilter
*/
function register()
{
static $registered = null;
if ($registered === null) {
$registered = 'stream-callback';
\stream_filter_register($registered, __NAMESPACE__ . '\CallbackFilter');
}
return $registered;
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Clue\StreamFilter;
// @codeCoverageIgnoreStart
if (!\function_exists(__NAMESPACE__ . '\\append')) {
require __DIR__ . '/functions.php';
}
// @codeCoverageIgnoreEnd