├── .github └── FUNDING.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── Edge.php ├── EdgeDirected.php ├── EdgeUndirected.php ├── Entity.php ├── Graph.php ├── Vertex.php └── Walk.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: clue 2 | custom: https://clue.engineering/support 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.9.3 (2021-12-30) 4 | 5 | * Feature: Support PHP 8.1 release. 6 | (#208 by @clue) 7 | 8 | * Fix: Fix automatic vertex ID generation when using vertex IDs with strings. 9 | (#204 by @viktorprogger) 10 | 11 | * Improve test suite and use GitHub Actions for continuous integration (CI). 12 | (#207 by @clue) 13 | 14 | ## 0.9.2 (2020-12-03) 15 | 16 | * Feature: Support PHP 8 and PHPUnit 9.3. 17 | (#200 by @SimonFrings) 18 | 19 | ## 0.9.1 (2019-10-02) 20 | 21 | * Fix: Deleting vertex with loop edge no longer fails. 22 | (#149 by @tomzx) 23 | 24 | * Fix: Fix returning directed loop edges and adjacent vertices from vertex twice. 25 | (#170 by @clue) 26 | 27 | * Minor documentation updates and fixes. 28 | (#153 by @marclaporte and #163, #164 and #172 by @clue) 29 | 30 | * Improve test suite to move tests to `Fhaculty\Graph\Tests` namespace, 31 | update test suite to support PHPUnit 6 and PHPUnit 5 and 32 | support running on legacy PHP 5.3 through PHP 7.2 and HHVM. 33 | (#148 by @tomzx and #150 and #162 by @clue) 34 | 35 | * Originally planned to add a new `AttributeAware::removeAttribute()` method, 36 | but reverted due to BC break. Change will be reconsidered for next major release. 37 | (#138 and #171 by @johnathanmdell and @clue) 38 | 39 | ## 0.9.0 (2015-03-07) 40 | 41 | * BC break: Split off individual components in order to stabilize core graph lib. 42 | ([#120](https://github.com/clue/graph/issues/120)) 43 | 44 | * Split off `Algorithm` namespace into separate [graphp/algorithms](https://github.com/graphp/algorithms) package. 45 | ([#119](https://github.com/clue/graph/issues/119)) 46 | 47 | * Split off `Exporter\TrivialGraphFormat` into separate [graphp/trivial-graph-format](https://github.com/graphp/trivial-graph-format) package. 48 | ([#121](https://github.com/clue/graph/issues/121)) 49 | 50 | * Split off `Loader` namespace into separate [graphp/plaintext](https://github.com/graphp/plaintext) package. 51 | ([#117](https://github.com/clue/graph/issues/117)) 52 | 53 | * BC break: Remove Exporter from `Graph` and `Graph::__toString()` (trivial graph format exporter has been split off). 54 | ([#122](https://github.com/clue/graph/pull/122)) 55 | 56 | * BC break: Vertices can no longer be sorted by (in/out)degree (degree algorithm has been split off). 57 | ([#128](https://github.com/clue/graph/pull/128)) 58 | 59 | * Apply PSR-4 layout under `src/` and add tests to achieve 100% test coverage. 60 | ([#127](https://github.com/clue/graph/issues/127) & [#129](https://github.com/clue/graph/issues/129)) 61 | 62 | ## 0.8.0 (2014-12-31) 63 | 64 | * Feature: Add general purpose Attributes. 65 | ([#103](https://github.com/clue/graph/pull/103)) 66 | 67 | * BC break: Split off all GraphViz-related classes to a separate 68 | [graphp/graphviz](https://github.com/graphp/graphviz) package. 69 | ([#115](https://github.com/clue/graph/pull/115)) 70 | 71 | * Feature: The base `Graph`, `Vertex` and `EdgeBase` classes can now be 72 | extended in order to implement a custom behavior. As such, one can now also 73 | instantiate them using the normal `new` operator instead of having to use 74 | `Graph::createVertex()` family of methods. 75 | ([#82](https://github.com/clue/graph/issues/82)) 76 | 77 | * BC break: Rename `Algorithm\Directed::isDirected()` to remove its ambiguity 78 | in regards to mixed and/or empty graphs 79 | ([#80](https://github.com/clue/graph/issues/80)) 80 | 81 | | Old name | New name | 82 | |---|---| 83 | | `Algorithm\Directed::isDirected()` | `Algorithm\Directed::hasDirected()` | 84 | 85 | * Feature: Add new `Algorithm\Directed::hasUndirected()` and 86 | `Algorithm\Directed::isMixed()` in order to complement the renamed 87 | `Algorithm\Directed::hasDirected()` 88 | ([#80](https://github.com/clue/graph/issues/80)) 89 | 90 | * BC break: `Walk::factoryCycleFromVertices()` no longer tries to auto-complete 91 | a cycle if the first vertex does not match the last one, but now throws an 92 | `InvalidArgumentException` instead ([#87](https://github.com/clue/graph/issues/87)) 93 | 94 | * Feature: Support loop `Walk`s, i.e. a walk with only a single edge from 95 | vertex A back to A ([#87](https://github.com/clue/graph/issues/87)) 96 | 97 | * Fix: Stricter checks for invalid cycles, such as one with an invalid 98 | predecessor-map or no edges at all ([#87](https://github.com/clue/graph/issues/87)) 99 | 100 | * Fix: The `Algorithm\ShortestPath\MooreBellmanFord` now also works for unweighted 101 | edges. This also fixes an issue where `Algorithm\DetectNegativeCycle` didn't work 102 | for unweighted edges. ([#81](https://github.com/clue/graph/issues/81)) 103 | 104 | * Fix: The `Algorithm\MinimumCostFlow` algorithms now work again. The reference 105 | to a non-existant class has been updated. Also fixed several issues with 106 | regards to special cases such as disconnected or undirected graphs. 107 | ([#74](https://github.com/clue/graph/issues/74)) 108 | 109 | * BC break: Remove unneeded alias definitions of `getVertexFirst()`, 110 | `getVertexSource()` and `getVertexTarget()` 111 | ([#76](https://github.com/clue/graph/issues/76)): 112 | 113 | | Old name | New name | 114 | |---|---| 115 | | `Graph::getVertexFirst()` | `Graph::getVertices()->getVertexFirst()` | 116 | | `Walk::getVertexSource()` | `Walk::getVertices()->getVertexFirst()` | 117 | | `Walk::getVertexTarget()` | `Walk::getVertices()->getVertexLast()` | 118 | 119 | ## 0.7.1 (2014-03-12) 120 | 121 | * Fix: Throwing an `UnexpectedValueException` if writing GraphViz Dot script 122 | to a temporary file fails and remove its debugging output 123 | ([#77](https://github.com/clue/graph/issues/77) and [#78](https://github.com/clue/graph/issues/78) @Metabor) 124 | 125 | * Fix: Improved GraphViz support for MS Windows 126 | ([#99](https://github.com/clue/graph/issues/99)) 127 | 128 | ## 0.7.0 (2013-09-11) 129 | 130 | * Feature: Add new `Set\Vertices` and `Set\Edges` classes that handle common 131 | operations on a Set of multiple `Vertex` and `Edge` instances respectively. 132 | ([#48](https://github.com/clue/graph/issues/48)) 133 | 134 | * BC break: Move operations and their corresponding constants concerning Sets 135 | to their corresponding Sets: 136 | 137 | | Old name | New name | 138 | |---|---| 139 | | `Edge\Base::getFirst()` | `Set\Edges::getEdgeOrder()` | 140 | | `Edge\Base::getAll()` | `Set\Edges::getEdgesOrder()` | 141 | | `Edge\Base::ORDER_*` | `Set\Edges::ORDER_*` | 142 | |---|---| 143 | | `Vertex::getFirst()` | `Set\Vertices::getVertexOrder()` | 144 | | `Vertex::getAll()` | `Set\Vertices::getVerticesOrder()` | 145 | | `Vertex::ORDER_` | `Set\Vertices::ORDER_*` | 146 | 147 | * BC break: Each `getVertices*()` and `getEdges*()` method now returns a `Set` 148 | instead of a primitive array of instances. *Most* of the time this should 149 | work without changing your code, because each `Set` implements an `Iterator` 150 | interface and can easily be iterated using `foreach`. However, using a `Set` 151 | instead of a plain array differs when checking its boolean value or 152 | comparing two Sets. I.e. if you happen to want to check if an `Set` is empty, 153 | you now have to use the more explicit syntax `$set->isEmpty()`. 154 | 155 | * BC break: `Vertex::getVertices()`, `Vertex::getVerticesEdgeTo()` and 156 | `Vertex::getVerticesEdgeFrom()` now return a `Set\Vertices` instance that 157 | may contain duplicate vertices if parallel (multiple) edges exist. Previously 158 | there was no easy way to detect this situation - this is now the default. If 159 | you also want to get unique / distinct `Vertex` instances, use 160 | `Vertex::getVertices()->getVerticesDistinct()` where applicable. 161 | 162 | * BC break: Remove all occurances of `getVerticesId()`, use 163 | `getVertices()->getIds()` instead. 164 | 165 | * BC break: Merge `Cycle` into `Walk` ([#61](https://github.com/clue/graph/issues/61)). 166 | As such, its static factory methods had to be renamed. Update your references if applicable: 167 | 168 | | Old name | New name | 169 | |---|---| 170 | | `Cycle::factoryFromPredecessorMap()` | `Walk::factoryCycleFromPredecessorMap()` | 171 | | `Cycle::factoryFromVertices()` | `Walk::factoryCycleFromVertices()` | 172 | | `Cycle::factoryFromEdges()` | `Walk::factoryCycleFromEdges()` | 173 | 174 | * BC break: Remove `Graph::isEmpty()` because it's not well-defined and might 175 | be confusing. Most literature suggests it should check for existing edges, 176 | whereas the old behavior was to check for existing vertices instead. Use either 177 | of the new and more transparent methods 178 | `Algorithm\Property\GraphProperty::isNull()` (old behavior) or (where applicable) 179 | `Algorithm\Property\GraphProperty::isEdgeless()` ([#63](https://github.com/clue/graph/issues/63)). 180 | 181 | * BC break: Each of the above methods (`Walk::factoryCycleFromPredecessorMap()`, 182 | `Walk::factoryCycleFromVertices()`, `Walk::factoryCycleFromEdges()`) now 183 | actually makes sure the returned `Walk` instance is actually a valid Cycle, 184 | i.e. the start `Vertex` is the same as the end `Vertex` ([#61](https://github.com/clue/graph/issues/61)) 185 | 186 | * BC break: Each `Algorithm\ShortestPath` algorithm now consistenly does not 187 | return a zero weight for the root Vertex and now supports loop edges on the root 188 | Vertex ([#62](https://github.com/clue/graph/issues/62)) 189 | 190 | * BC break: Each `Algorithm\ShortestPath` algorithm now consistently throws an 191 | `OutOfBoundsException` for unreachable vertices 192 | ([#62](https://github.com/clue/graph/issues/62)) 193 | 194 | * BC break: A null Graph (a Graph with no Vertices and thus no Edges) is not a 195 | valid tree (because it is not connected), adjust `Algorithm\Tree\Base::isTree()` 196 | accordingly. 197 | ([#72](https://github.com/clue/graph/issues/72)) 198 | 199 | * BC break: Remove all occurances of `getNumberOfVertices()` and 200 | `getNumberOfEdges()` ([#75](https://github.com/clue/graph/issues/75) and 201 | [#48](https://github.com/clue/graph/issues/48)): 202 | 203 | | Old name | New name | 204 | |---|---| 205 | | `$set->getNumberOfVertices()` | `count($set->getVertices())` | 206 | | `$set->getNumberOfEdges()` | `count($set->getEdges())` | 207 | 208 | * BC break: Replace base `Set` class with `Set\DualAggregate` interface. This 209 | is unlikely to affect you, but might potentially break your custom 210 | inheritance or polymorphism for algorithms. 211 | ([#75](https://github.com/clue/graph/issues/75)) 212 | 213 | * Feature: Add `Algorithm\ShortestPath\Base::hasVertex(Vertex $vertex)` to check whether 214 | a path to the given Vertex exists ([#62](https://github.com/clue/graph/issues/62)). 215 | 216 | * Feature: Support opening GraphViz images on Mac OS X in default image viewer 217 | ([#67](https://github.com/clue/graph/issues/67) @onigoetz) 218 | 219 | * Feature: Add `Algorithm\MinimumSpanningTree\Base::getWeight()` to get total 220 | weight of resulting minimum spanning tree (MST). 221 | ([#73](https://github.com/clue/graph/issues/73)) 222 | 223 | * Feature: Each `Algorithm\MinimumSpanningTree` algorithm now supports 224 | undirected and mixed Graphs, as well as null weights for Edges. 225 | ([#73](https://github.com/clue/graph/issues/73)) 226 | 227 | * BC break: Each `Algorithm\MinimumSpanningTree` algorithm now throws an 228 | `UnexpectedValueException` for unconnected Graphs (and thus also null Graphs). 229 | ([#73](https://github.com/clue/graph/issues/73)) 230 | 231 | * Feature: Add `Walk::factoryFromVertices()` 232 | ([#64](https://github.com/clue/graph/issues/64)). 233 | 234 | * Fix: Checking `Walk::isValid()` 235 | ([#61](https://github.com/clue/graph/issues/61)) 236 | 237 | * Fix: Missing import prevented 238 | `Algorithm\ShortestPath\MooreBellmanFord::getCycleNegative()` from actually 239 | throwing the right `UnderflowException` if no cycle was found 240 | ([#62](https://github.com/clue/graph/issues/62)) 241 | 242 | * Fix: Calling `Exporter\Image::setFormat()` had no effect due to misassignment 243 | ([#70](https://github.com/clue/graph/issues/70) @FGM) 244 | 245 | ## 0.6.0 (2013-07-11) 246 | 247 | * BC break: Move algorithm definitions in base classes to separate algorithm classes ([#27](https://github.com/clue/graph/issues/27)). 248 | The following methods containing algorithms were now moved to separate algorithm classes. This 249 | change encourages code-reuse, simplifies spotting algorithms, helps reducing complexity, 250 | improves testablity and avoids tight coupling. Update your references if applicable: 251 | 252 | | Old name | New name | Related ticket | 253 | |---|---|---| 254 | | `Set::getWeight()` | `Algorithm\Weight::getWeight()` | [#33](https://github.com/clue/graph/issues/33) | 255 | | `Set::getWeightFlow()` | `Algorithm\Weight::getWeightFlow()` | [#33](https://github.com/clue/graph/issues/33) | 256 | | `Set::getWeightMin()` | `Algorithm\Weight::getWeightMin()` | [#33](https://github.com/clue/graph/issues/33) | 257 | | `Set::isWeighted()` | `Algorithm\Weight::isWeighted()` | [#33](https://github.com/clue/graph/issues/33) | 258 | |-|-|-| 259 | | `Graph::getDegree()` | `Algorithm\Degree::getDegree()` | [#29](https://github.com/clue/graph/issues/29) | 260 | | `Graph::getDegreeMin()` | `Algorithm\Degree::getDegreeMin()` | [#29](https://github.com/clue/graph/issues/29) | 261 | | `Graph::getDegreeMax()` | `Algorithm\Degree::getDegreeMax()` | [#29](https://github.com/clue/graph/issues/29) | 262 | | `Graph::isRegular()` | `Algorithm\Degree::isRegular()` | [#29](https://github.com/clue/graph/issues/29) | 263 | | `Graph::isBalanced()` | `Algorithm\Degree::isBalanced()` | [#29](https://github.com/clue/graph/issues/29) | 264 | | `Vertex::getDegree()` | `Algorithm\Degree:getDegreeVertex()` | [#49](https://github.com/clue/graph/issues/49) | 265 | | `Vertex::getDegreeIn()` | `Algorithm\Degree:getDegreeInVertex()` | [#49](https://github.com/clue/graph/issues/49) | 266 | | `Vertex::getDegreeOut()` | `Algorithm\Degree:getDegreeOutVertex()` | [#49](https://github.com/clue/graph/issues/49) | 267 | | `Vertex::isSink()` | `Algorithm\Degree:isVertexSink()` | [#49](https://github.com/clue/graph/issues/49) | 268 | | `Vertex::isSource()` | `Algorithm\Degree:isVertexSource()` | [#49](https://github.com/clue/graph/issues/49) | 269 | | `Vertex::isIsolated()` | `Algorithm\Degree::isVertexIsolated()` | [#49](https://github.com/clue/graph/issues/49) | 270 | |-|-|-| 271 | | `Set::isDirected()` | `Algorithm\Directed::isDirected()` | [#34](https://github.com/clue/graph/issues/34) | 272 | |-|-|-| 273 | | `Graph::isSymmetric()` | `Algorithm\Symmetric::isSymmetric()` | [#41](https://github.com/clue/graph/issues/41) | 274 | |-|-|-| 275 | | `Graph::isComplete()` | `Algorithm\Complete::isComplete()` | [#43](https://github.com/clue/graph/issues/43) | 276 | |-|-|-| 277 | | `Set::hasFlow()` | `Algorithm\Flow::hasFlow()` | [#47](https://github.com/clue/graph/issues/47) | 278 | | `Graph::getBalance()` | `Algorithm\Flow::getBalance()` | [#30](https://github.com/clue/graph/issues/30), [#47](https://github.com/clue/graph/issues/47) | 279 | | `Graph::isBalancedFlow()` | `Algorithm\Flow::isBalancedFlow()` | [#30](https://github.com/clue/graph/issues/39), [#47](https://github.com/clue/graph/issues/47) | 280 | | `Vertex::getFlow()` | `Algorithm\Flow::getFlowVertex()` | [#47](https://github.com/clue/graph/issues/47) | 281 | |-|-|-| 282 | | `Vertex::isLeaf()` | `Algorithm\Tree\Undirected::isVertexLeaf()` | [#44](https://github.com/clue/graph/issues/44) | 283 | |-|-|-| 284 | | `Set::hasLoop()` | `Algorithm\Loop::hasLoop()` | [#51](https://github.com/clue/graph/issues/51) | 285 | | `Vertex::hasLoop()` | `Algorithm\Loop::hasLoopVertex()` | [#51](https://github.com/clue/graph/issues/51) | 286 | |-|-|-| 287 | | `Set::hasEdgeParallel()` | `Algorithm\Parallel::hasEdgeParallel()` | [#52](https://github.com/clue/graph/issues/52) | 288 | | `Edge\Base::hasEdgeParallel()` | `Algorithm\Parallel::hasEdgeParallelEdge()` | [#52](https://github.com/clue/graph/issues/52) | 289 | | `Edge\Base::getEdgesParallel()` | `Algorithm\Parallel::getEdgeParallelEdge()` | [#52](https://github.com/clue/graph/issues/52) | 290 | |-|-|-| 291 | | `Graph::isEdgeless()` | `Algorithm\Property\GraphProperty::isEdgeless()` | [#54](https://github.com/clue/graph/issues/54) | 292 | | `Graph::isTrivial()` | `Algorithm\Property\GraphProperty::isTrivial()` | [#54](https://github.com/clue/graph/issues/54) | 293 | | `Walk::isCycle()` | `Algorithm\Property\WalkProperty::isCycle()` | [#54](https://github.com/clue/graph/issues/54) | 294 | | `Walk::isPath()` | `Algorithm\Property\WalkProperty::isPath()` | [#54](https://github.com/clue/graph/issues/54) | 295 | | `Walk::hasCycle()` | `Algorithm\Property\WalkProperty::hasCycle()` | [#54](https://github.com/clue/graph/issues/54) | 296 | | `Walk::isLoop()` | `Algorithm\Property\WalkProperty::isLoop()` | [#54](https://github.com/clue/graph/issues/54) | 297 | | `Walk::isDigon()` | `Algorithm\Property\WalkProperty::isDigon()` | [#54](https://github.com/clue/graph/issues/54) | 298 | | `Walk::isTriangle()` | `Algorithm\Property\WalkProperty::isTriangle()` | [#54](https://github.com/clue/graph/issues/54) | 299 | | `Walk::isSimple()` | `Algorithm\Property\WalkProperty::isSimple()` | [#54](https://github.com/clue/graph/issues/54) | 300 | | `Walk::isHamiltonian()` | `Algorithm\Property\WalkProperty::isHamiltonian()` | [#54](https://github.com/clue/graph/issues/54) | 301 | | `Walk::isEulerian()` | `Algorithm\Property\WalkProperty::isEulerian()` | [#54](https://github.com/clue/graph/issues/54) | 302 | 303 | * 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* 304 | have been removed, their original/actual name has already existed before and continues to work 305 | unchanged. Update your references if applicable: 306 | 307 | | Old/removed alias definition | Actual name | 308 | |---|---| 309 | | `Graph::isConnected()` | `Algorithm\ConnectedComponents::isSingle()` | 310 | | `Graph::hasEulerianCycle()` | `Algorithm\Eulerian::hasCycle()` | 311 | | `Graph::getNumberOfComponents()` | `Algorithm\ConnectedComponents::getNumberOfComponents()` | 312 | | `Graph::getNumberOfGroups()` | `Algorithm\Groups::getNumberOfGroups()` | 313 | | `Graph::isBipartit()` | `Algorithm\Bipartit::isBipartit()` | 314 | | `Vertex::hasPathTo()` | `Algorithm\ShortestPath\BreadthFirst::hasVertex()` | 315 | | `Vertex::hasPathFrom()` | `Algorithm\ShortestPath\BreadthFirst::hasVertex()` | 316 | | `Vertex::getVerticesPathTo()` | `Algorithm\ShortestPath\BreadthFirst::getVertices()` | 317 | | `Vertex::getVerticesPathFrom()` | `Algorithm\ShortestPath\BreadthFirst::getVertices()` | 318 | 319 | * BC break: `Graph::createVertices()` now returns an array of vertices instead of the 320 | chainable `Graph` ([#19](https://github.com/clue/graph/issues/19)) 321 | 322 | * BC break: Move `Loader\UmlClassDiagram` to separate [fhaculty/graph-uml](https://github.com/fhaculty/graph-uml) 323 | repo ([#38](https://github.com/clue/graph/issues/38)) 324 | 325 | * BC break: Remove needless `Algorithm\MinimumSpanningTree\PrimWithIf` 326 | (use `Algorithm\MinimumSpanningTree\Prim` instead) 327 | ([#45](https://github.com/clue/graph/issues/45)) 328 | 329 | * BC break: `Vertex::createEdgeTo()` now returns an instance of type 330 | `Edge\Undirected` instead of `Edge\UndirectedId` 331 | ([#46](https://github.com/clue/graph/issues/46)) 332 | 333 | * BC break: `Edge\Base::setCapacity()` now consistently throws an `RangeException` 334 | instead of `InvalidArgumentException` if the current flow exceeds the new maximum 335 | capacity ([#53](https://github.com/clue/graph/issues/53)) 336 | 337 | * Feature: New `Algorithm\Tree` namespace with algorithms for undirected and directed, 338 | rooted trees ([#44](https://github.com/clue/graph/issues/44)) 339 | 340 | * Feature: According to be above list of moved algorithm methods, the following algorithm 341 | classes have been added ([#27](https://github.com/clue/graph/issues/27)): 342 | * New `Algorithm\Weight` ([#33](https://github.com/clue/graph/issues/33)) 343 | * New `Algorithm\Degree` ([#29](https://github.com/clue/graph/issues/29), [#49](https://github.com/clue/graph/issues/49)) 344 | * New `Algorithm\Directed` ([#34](https://github.com/clue/graph/issues/34)) 345 | * New `Algorithm\Symmetric` ([#41](https://github.com/clue/graph/issues/41)) 346 | * New `Algorithm\Complete` ([#43](https://github.com/clue/graph/issues/43)) 347 | * New `Algorithm\Flow` ([#30](https://github.com/clue/graph/issues/30), [#47](https://github.com/clue/graph/issues/47)) 348 | * New `Algorithm\Tree` ([#44](https://github.com/clue/graph/issues/44)) 349 | * New `Algorithm\Loop` ([#51](https://github.com/clue/graph/issues/51)) 350 | * New `Algorithm\Parallel` ([#52](https://github.com/clue/graph/issues/52)) 351 | * New `Algorithm\Property` ([#54](https://github.com/clue/graph/issues/54)) 352 | 353 | * Feature: `Graph::createVertices()` now also accepts an array of vertex IDs 354 | ([#19](https://github.com/clue/graph/issues/19)) 355 | 356 | * Feature: Add `Algorithm\Property\WalkProperty::hasLoop()` alias definition for 357 | completeness ([#54](https://github.com/clue/graph/issues/54)) 358 | 359 | * Feature: Add `Algorithm\Property\WalkProperty::isCircuit()` definition to distinguish 360 | circuits from cycles ([#54](https://github.com/clue/graph/issues/54)) 361 | 362 | * Fix: Checking hamiltonian cycles always returned false 363 | ([#54](https://github.com/clue/graph/issues/54)) 364 | 365 | * Fix: A Walk with no edges is no longer considered a valid cycle 366 | ([#54](https://github.com/clue/graph/issues/54)) 367 | 368 | * Fix: Various issues with `Vertex`/`Edge` layout attributes 369 | ([#32](https://github.com/clue/graph/issues/32)) 370 | 371 | * Fix: Getting multiple parallel edges for undirected edges 372 | ([#52](https://github.com/clue/graph/issues/52)) 373 | 374 | ## 0.5.0 (2013-05-07) 375 | 376 | * First tagged release (See issue [#20](https://github.com/clue/graph/issues/20) for more info on why it starts as v0.5.0) 377 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012+ Christian Lück (Maintainer) 4 | Copyright (c) 2012+ Graphp Core Team and our awesome contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is furnished 11 | to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphp/graph 2 | 3 | [![CI status](https://github.com/graphp/graph/actions/workflows/ci.yml/badge.svg)](https://github.com/graphp/graph/actions) 4 | [![development installs on Packagist](https://img.shields.io/packagist/dt/graphp/graph?color=blue&label=development%20installs%20on%20Packagist)](https://packagist.org/packages/graphp/graph) 5 | [![previous installs on Packagist](https://img.shields.io/packagist/dt/clue/graph?color=blue&label=previous%20installs%20on%20Packagist)](https://packagist.org/packages/clue/graph) 6 | 7 | GraPHP is the mathematical graph/network library written in PHP. 8 | 9 | > **Development version:** This branch contains the code for the upcoming 1.0 release. 10 | > For the code of the current stable 0.9 release, check out the 11 | > [`0.9.x` branch](https://github.com/graphp/graph/tree/0.9.x). 12 | > 13 | > The upcoming 1.0 release will be the way forward for this package. 14 | > However, we will still actively support 0.9.x for those not yet 15 | > on the latest version. 16 | > See also [installation instructions](#install) for more details. 17 | 18 | **Table of contents** 19 | 20 | * [Quickstart examples](#quickstart-examples) 21 | * [Features](#features) 22 | * [Components](#components) 23 | * [Graph drawing](#graph-drawing) 24 | * [Common algorithms](#common-algorithms) 25 | * [Install](#install) 26 | * [Tests](#tests) 27 | * [Contributing](#contributing) 28 | * [License](#license) 29 | 30 | ## Quickstart examples 31 | 32 | Once [installed](#install), let's initialize a sample graph: 33 | 34 | ```php 35 | createVertex(array('name' => 'Rome')); 43 | $madrid = $graph->createVertex(array('name' => 'Madrid')); 44 | $cologne = $graph->createVertex(array('name' => 'Cologne')); 45 | 46 | // build some roads 47 | $graph->createEdgeDirected($cologne, $madrid); 48 | $graph->createEdgeDirected($madrid, $rome); 49 | // create loop 50 | $graph->createEdgeDirected($rome, $rome); 51 | ``` 52 | 53 | Let's see which city (Vertex) has a road (i.e. an edge pointing) to Rome: 54 | 55 | ```php 56 | foreach ($rome->getVerticesEdgeFrom() as $vertex) { 57 | echo $vertex->getAttribute('name') . ' leads to Rome' . PHP_EOL; 58 | // result: Madrid and Rome itself lead to Rome 59 | } 60 | ``` 61 | 62 | ## Features 63 | 64 | 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). 65 | 66 | 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. 67 | 68 | ## Components 69 | 70 | This library provides the core data structures for working with graphs, its vertices, edges and attributes. 71 | 72 | There are several official components built on top of these structures to provide commonly needed functionality. 73 | This architecture allows these components to be used independently and on demand only. 74 | 75 | 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). 76 | 77 | ### Graph drawing 78 | 79 | 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. 80 | 81 | See [graphp/graphviz](https://github.com/graphp/graphviz) for more details. 82 | 83 | ### Common algorithms 84 | 85 | Besides graph drawing, one of the most common things to do with graphs is running algorithms to solve common graph problems. 86 | Therefore this library is being used as the basis for implementations for a number of commonly used graph algorithms: 87 | 88 | * Search 89 | * Deep first (DFS) 90 | * Breadth first search (BFS) 91 | * Shortest path 92 | * [Dijkstra](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm) 93 | * Moore-Bellman-Ford (MBF) 94 | * Counting number of hops (simple BFS) 95 | * [Minimum spanning tree (MST)](https://en.wikipedia.org/wiki/Minimum_spanning_tree) 96 | * Kruskal 97 | * Prim 98 | * [Traveling salesman problem (TSP)](https://en.wikipedia.org/wiki/Travelling_salesman_problem) 99 | * Bruteforce algorithm 100 | * Minimum spanning tree heuristic (TSP MST heuristic) 101 | * Nearest neighbor heuristic (NN heuristic) 102 | * Maximum flow 103 | * [Edmonds-Karp](https://en.wikipedia.org/wiki/Edmonds%E2%80%93Karp_algorithm) 104 | * Minimum cost flow (MCF) 105 | * Cycle canceling 106 | * Successive shortest path 107 | * Maximum matching 108 | * Flow algorithm 109 | 110 | See [graphp/algorithms](https://github.com/graphp/algorithms) for more details. 111 | 112 | ## Install 113 | 114 | The recommended way to install this library is [through Composer](https://getcomposer.org/). 115 | [New to Composer?](https://getcomposer.org/doc/00-intro.md) 116 | 117 | Once released, this project will follow [SemVer](https://semver.org/). 118 | At the moment, this will install the latest development version: 119 | 120 | ```bash 121 | composer require graphp/graph:^1@dev 122 | ``` 123 | 124 | See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. 125 | 126 | This project aims to run on any platform and thus does not require any PHP 127 | extensions and supports running on legacy PHP 5.3 through current PHP 8+. 128 | It's *highly recommended to use the latest supported PHP version* for this project. 129 | 130 | You may also want to install some of the [additional components](#components). 131 | A list of all official components can be found in the [graphp project](https://github.com/graphp). 132 | 133 | ## Tests 134 | 135 | This library uses PHPUnit for its extensive test suite. 136 | To run the test suite, you first need to clone this repo and then install all 137 | dependencies [through Composer](https://getcomposer.org/): 138 | 139 | ```bash 140 | composer install 141 | ``` 142 | 143 | To run the test suite, go to the project root and run: 144 | 145 | ```bash 146 | vendor/bin/phpunit 147 | ``` 148 | 149 | ## Contributing 150 | 151 | This library comes with an extensive test suite and is regularly tested and used in the *real world*. 152 | Despite this, this library is still considered beta software and its API is subject to change. 153 | The [changelog](CHANGELOG.md) lists all relevant information for updates between releases. 154 | 155 | 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. 156 | 157 | Besides directly working with the code, any additional documentation, additions to our readme or even fixing simple typos are appreciated just as well. 158 | 159 | Any feedback and/or contribution is welcome! 160 | 161 | Check out #graphp on irc.freenode.net. 162 | 163 | ## License 164 | 165 | This project is released under the permissive [MIT license](LICENSE). 166 | 167 | > Did you know that I offer custom development services and issuing invoices for 168 | sponsorships of releases and for contributions? Contact me (@clue) for details. 169 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphp/graph", 3 | "type": "library", 4 | "description": "GraPHP is the mathematical graph/network library written in PHP.", 5 | "keywords": [ 6 | "graph", 7 | "network", 8 | "mathematical", 9 | "vertex", 10 | "edge" 11 | ], 12 | "homepage": "https://github.com/graphp/graph", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Christian Lück", 17 | "email": "christian@clue.engineering" 18 | } 19 | ], 20 | "require": { 21 | "php": ">=5.3" 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" 25 | }, 26 | "suggest": { 27 | "graphp/graphviz": "GraphViz graph drawing / DOT output", 28 | "graphp/algorithms": "Common graph algorithms, such as Dijkstra and Moore-Bellman-Ford (shortest path), minimum spanning tree (MST), Kruskal, Prim and many more.." 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "Graphp\\Graph\\": "src/" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Graphp\\Graph\\Tests\\": "tests/" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Edge.php: -------------------------------------------------------------------------------- 1 | 19 | * @return Vertex[] 20 | */ 21 | abstract public function getVerticesTarget(); 22 | 23 | /** 24 | * get vertices that are the start of this edge 25 | * 26 | * @psalm-return list 27 | * @return Vertex[] 28 | */ 29 | abstract public function getVerticesStart(); 30 | 31 | /** 32 | * 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) 33 | * 34 | * @param Vertex $startVertex 35 | * @return bool 36 | * @uses Vertex::getVertexToFrom() 37 | */ 38 | abstract public function hasVertexStart(Vertex $startVertex); 39 | 40 | /** 41 | * 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) 42 | * 43 | * @param Vertex $targetVertex 44 | * @return bool 45 | * @uses Vertex::getVertexFromTo() 46 | */ 47 | abstract function hasVertexTarget(Vertex $targetVertex); 48 | 49 | abstract public function isConnection(Vertex $from, Vertex $to); 50 | 51 | /** 52 | * returns whether this edge is actually a loop 53 | * 54 | * @return bool 55 | */ 56 | abstract public function isLoop(); 57 | 58 | /** 59 | * get target vertex we can reach with this edge from the given start vertex 60 | * 61 | * @param Vertex $startVertex 62 | * @return Vertex 63 | * @throws \InvalidArgumentException if given $startVertex is not a valid start 64 | * @see self::hasEdgeFrom() to check if given start is valid 65 | */ 66 | abstract public function getVertexToFrom(Vertex $startVertex); 67 | 68 | /** 69 | * get start vertex which can reach us(the given end vertex) with this edge 70 | * 71 | * @param Vertex $endVertex 72 | * @return Vertex 73 | * @throws \InvalidArgumentException if given $startVertex is not a valid end 74 | * @see self::hasEdgeFrom() to check if given start is valid 75 | */ 76 | abstract public function getVertexFromTo(Vertex $endVertex); 77 | 78 | /** 79 | * get list of all vertices this edge connects 80 | * 81 | * @psalm-return list 82 | * @return Vertex[] 83 | */ 84 | abstract public function getVertices(); 85 | 86 | /** 87 | * get graph instance this edge is attached to 88 | * 89 | * @return Graph 90 | */ 91 | public function getGraph() 92 | { 93 | $vertices = $this->getVertices(); 94 | $vertex = \reset($vertices); 95 | \assert($vertex instanceof Vertex); 96 | 97 | return $vertex->getGraph(); 98 | } 99 | 100 | /** 101 | * do NOT allow cloning of objects 102 | * 103 | * @throws \BadMethodCallException 104 | * @codeCoverageIgnore 105 | */ 106 | private function __clone() 107 | { 108 | throw new \BadMethodCallException(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/EdgeDirected.php: -------------------------------------------------------------------------------- 1 | getGraph() !== $to->getGraph()) { 34 | throw new \InvalidArgumentException('Vertices have to be within the same graph'); 35 | } 36 | 37 | $this->from = $from; 38 | $this->to = $to; 39 | $this->attributes = $attributes; 40 | 41 | $from->getGraph()->addEdge($this); 42 | $from->addEdge($this); 43 | $to->addEdge($this); 44 | } 45 | 46 | public function getVerticesTarget() 47 | { 48 | return array($this->to); 49 | } 50 | 51 | public function getVerticesStart() 52 | { 53 | return array($this->from); 54 | } 55 | 56 | public function getVertices() 57 | { 58 | return array($this->from, $this->to); 59 | } 60 | 61 | /** 62 | * get end/target vertex 63 | * 64 | * @return Vertex 65 | */ 66 | public function getVertexEnd() 67 | { 68 | return $this->to; 69 | } 70 | 71 | /** 72 | * get start vertex 73 | * 74 | * @return Vertex 75 | */ 76 | public function getVertexStart() 77 | { 78 | return $this->from; 79 | } 80 | 81 | public function isConnection(Vertex $from, Vertex $to) 82 | { 83 | return ($this->to === $to && $this->from === $from); 84 | } 85 | 86 | public function isLoop() 87 | { 88 | return ($this->to === $this->from); 89 | } 90 | 91 | public function getVertexToFrom(Vertex $startVertex) 92 | { 93 | if ($this->from !== $startVertex) { 94 | throw new \InvalidArgumentException('Invalid start vertex'); 95 | } 96 | 97 | return $this->to; 98 | } 99 | 100 | public function getVertexFromTo(Vertex $endVertex) 101 | { 102 | if ($this->to !== $endVertex) { 103 | throw new \InvalidArgumentException('Invalid end vertex'); 104 | } 105 | 106 | return $this->from; 107 | } 108 | 109 | public function hasVertexStart(Vertex $startVertex) 110 | { 111 | return ($this->from === $startVertex); 112 | } 113 | 114 | public function hasVertexTarget(Vertex $targetVertex) 115 | { 116 | return ($this->to === $targetVertex); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/EdgeUndirected.php: -------------------------------------------------------------------------------- 1 | getGraph() !== $b->getGraph()) { 34 | throw new \InvalidArgumentException('Vertices have to be within the same graph'); 35 | } 36 | 37 | $this->a = $a; 38 | $this->b = $b; 39 | $this->attributes = $attributes; 40 | 41 | $a->getGraph()->addEdge($this); 42 | $a->addEdge($this); 43 | $b->addEdge($this); 44 | } 45 | 46 | public function getVerticesTarget() 47 | { 48 | return array($this->b, $this->a); 49 | } 50 | 51 | public function getVerticesStart() 52 | { 53 | return array($this->a, $this->b); 54 | } 55 | 56 | public function getVertices() 57 | { 58 | return array($this->a, $this->b); 59 | } 60 | 61 | public function isConnection(Vertex $from, Vertex $to) 62 | { 63 | // one way or other way 64 | return (($this->a === $from && $this->b === $to) || ($this->b === $from && $this->a === $to)); 65 | } 66 | 67 | public function isLoop() 68 | { 69 | return ($this->a === $this->b); 70 | } 71 | 72 | public function getVertexToFrom(Vertex $startVertex) 73 | { 74 | if ($this->a === $startVertex) { 75 | return $this->b; 76 | } elseif ($this->b === $startVertex) { 77 | return $this->a; 78 | } else { 79 | throw new \InvalidArgumentException('Invalid start vertex'); 80 | } 81 | } 82 | 83 | public function getVertexFromTo(Vertex $endVertex) 84 | { 85 | if ($this->a === $endVertex) { 86 | return $this->b; 87 | } elseif ($this->b === $endVertex) { 88 | return $this->a; 89 | } else { 90 | throw new \InvalidArgumentException('Invalid end vertex'); 91 | } 92 | } 93 | 94 | public function hasVertexStart(Vertex $startVertex) 95 | { 96 | return ($this->a === $startVertex || $this->b === $startVertex); 97 | } 98 | 99 | public function hasVertexTarget(Vertex $targetVertex) 100 | { 101 | // same implementation as direction does not matter 102 | return $this->hasVertexStart($targetVertex); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Entity.php: -------------------------------------------------------------------------------- 1 | attributes[$name]) ? $this->attributes[$name] : $default; 28 | } 29 | 30 | /** 31 | * set a single attribute with the given $name to given $value 32 | * 33 | * @param string $name 34 | * @param mixed $value 35 | * @return $this chainable 36 | */ 37 | public function setAttribute($name, $value) 38 | { 39 | $this->attributes[$name] = $value; 40 | 41 | return $this; 42 | } 43 | 44 | /** 45 | * Removes a single attribute with the given $name 46 | * 47 | * @param string $name 48 | * @return $this chainable 49 | */ 50 | public function removeAttribute($name) 51 | { 52 | unset($this->attributes[$name]); 53 | 54 | return $this; 55 | } 56 | 57 | /** 58 | * get an array of all attributes 59 | * 60 | * @return array 61 | */ 62 | public function getAttributes() 63 | { 64 | return $this->attributes; 65 | } 66 | 67 | /** 68 | * set an array of additional attributes 69 | * 70 | * @param array $attributes 71 | * @return $this chainable 72 | */ 73 | public function setAttributes(array $attributes) 74 | { 75 | $this->attributes = $attributes + $this->attributes; 76 | 77 | return $this; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Graph.php: -------------------------------------------------------------------------------- 1 | attributes = $attributes; 16 | } 17 | 18 | /** 19 | * return list of all vertices added to this graph 20 | * 21 | * @psalm-return list 22 | * @return Vertex[] 23 | */ 24 | public function getVertices() 25 | { 26 | return $this->vertices; 27 | } 28 | 29 | /** 30 | * return list of all edges added to this graph 31 | * 32 | * @psalm-return list 33 | * @return Edge[] 34 | */ 35 | public function getEdges() 36 | { 37 | return $this->edges; 38 | } 39 | 40 | /** 41 | * create a new Vertex in the Graph 42 | * 43 | * @param array $attributes 44 | * @return Vertex 45 | */ 46 | public function createVertex(array $attributes = array()) 47 | { 48 | return new Vertex($this, $attributes); 49 | } 50 | 51 | /** 52 | * Creates a new undirected (bidirectional) edge between the given two vertices. 53 | * 54 | * @param Vertex $a 55 | * @param Vertex $b 56 | * @param array $attributes 57 | * @return EdgeUndirected 58 | * @throws \InvalidArgumentException 59 | */ 60 | public function createEdgeUndirected(Vertex $a, Vertex $b, array $attributes = array()) 61 | { 62 | if ($a->getGraph() !== $this) { 63 | throw new \InvalidArgumentException('Vertices have to be within this graph'); 64 | } 65 | 66 | return new EdgeUndirected($a, $b, $attributes); 67 | } 68 | 69 | /** 70 | * Creates a new directed edge from the given start vertex to given target vertex 71 | * 72 | * @param Vertex $source source vertex 73 | * @param Vertex $target target vertex 74 | * @param array $attributes 75 | * @return EdgeDirected 76 | * @throws \InvalidArgumentException 77 | */ 78 | public function createEdgeDirected(Vertex $source, Vertex $target, array $attributes = array()) 79 | { 80 | if ($source->getGraph() !== $this) { 81 | throw new \InvalidArgumentException('Vertices have to be within this graph'); 82 | } 83 | 84 | return new EdgeDirected($source, $target, $attributes); 85 | } 86 | 87 | /** 88 | * Returns a copy of this graph without the given vertex 89 | * 90 | * If this vertex was not found in this graph, the returned graph will be 91 | * identical. 92 | * 93 | * @param Vertex $vertex 94 | * @return self 95 | */ 96 | public function withoutVertex(Vertex $vertex) 97 | { 98 | return $this->withoutVertices(array($vertex)); 99 | } 100 | 101 | /** 102 | * Returns a copy of this graph without the given vertices 103 | * 104 | * If any of the given vertices can not be found in this graph, they will 105 | * silently be ignored. If neither of the vertices can be found in this graph, 106 | * the returned graph will be identical. 107 | * 108 | * @param Vertex[] $vertices 109 | * @return self 110 | */ 111 | public function withoutVertices(array $vertices) 112 | { 113 | // keep copy of original vertices and edges and temporarily remove all $vertices and their adjacent edges 114 | $originalEdges = $this->edges; 115 | $originalVertices = $this->vertices; 116 | foreach ($vertices as $vertex) { 117 | if (($key = \array_search($vertex, $this->vertices, true)) !== false) { 118 | unset($this->vertices[$key]); 119 | foreach ($vertex->getEdges() as $edge) { 120 | if (($key = \array_search($edge, $this->edges, true)) !== false) { 121 | unset($this->edges[$key]); 122 | } 123 | } 124 | } 125 | } 126 | 127 | // no vertices matched => return graph as-is 128 | if (\count($this->vertices) === \count($originalVertices)) { 129 | return $this; 130 | } 131 | 132 | // clone graph with vertices/edges temporarily removed, then restore 133 | $clone = clone $this; 134 | $this->edges = $originalEdges; 135 | $this->vertices = $originalVertices; 136 | 137 | return $clone; 138 | } 139 | 140 | /** 141 | * Returns a copy of this graph without the given edge 142 | * 143 | * If this edge was not found in this graph, the returned graph will be 144 | * identical. 145 | * 146 | * @param Edge $edge 147 | * @return self 148 | */ 149 | public function withoutEdge(Edge $edge) 150 | { 151 | return $this->withoutEdges(array($edge)); 152 | } 153 | 154 | /** 155 | * Returns a copy of this graph without the given edges 156 | * 157 | * If any of the given edges can not be found in this graph, they will 158 | * silently be ignored. If neither of the edges can be found in this graph, 159 | * the returned graph will be identical. 160 | * 161 | * @param Edge[] $edges 162 | * @return self 163 | */ 164 | public function withoutEdges(array $edges) 165 | { 166 | // keep copy of original edges and temporarily remove all $edges 167 | $original = $this->edges; 168 | foreach ($edges as $edge) { 169 | if (($key = \array_search($edge, $this->edges, true)) !== false) { 170 | unset($this->edges[$key]); 171 | } 172 | } 173 | 174 | // no edges matched => return graph as-is 175 | if (\count($this->edges) === \count($original)) { 176 | return $this; 177 | } 178 | 179 | // clone graph with edges temporarily removed, then restore 180 | $clone = clone $this; 181 | $this->edges = $original; 182 | 183 | return $clone; 184 | } 185 | 186 | /** 187 | * adds a new Vertex to the Graph (MUST NOT be called manually!) 188 | * 189 | * @param Vertex $vertex instance of the new Vertex 190 | * @return void 191 | * @internal 192 | * @see self::createVertex() instead! 193 | */ 194 | public function addVertex(Vertex $vertex) 195 | { 196 | $this->vertices[] = $vertex; 197 | } 198 | 199 | /** 200 | * adds a new Edge to the Graph (MUST NOT be called manually!) 201 | * 202 | * @param Edge $edge instance of the new Edge 203 | * @return void 204 | * @internal 205 | * @see Graph::createEdgeUndirected() instead! 206 | */ 207 | public function addEdge(Edge $edge) 208 | { 209 | $this->edges []= $edge; 210 | } 211 | 212 | /** 213 | * create new clone/copy of this graph - copy all attributes, vertices and edges 214 | */ 215 | public function __clone() 216 | { 217 | $vertices = $this->vertices; 218 | $this->vertices = array(); 219 | 220 | $edges = $this->edges; 221 | $this->edges = array(); 222 | 223 | $map = array(); 224 | foreach ($vertices as $originalVertex) { 225 | \assert($originalVertex instanceof Vertex); 226 | 227 | $vertex = new Vertex($this, $originalVertex->getAttributes()); 228 | 229 | // create map with old vertex hash to new vertex object 230 | $map[\spl_object_hash($originalVertex)] = $vertex; 231 | } 232 | 233 | foreach ($edges as $originalEdge) { 234 | \assert($originalEdge instanceof Edge); 235 | 236 | // use map to match old vertex hashes to new vertex objects 237 | $vertices = $originalEdge->getVertices(); 238 | $v1 = $map[\spl_object_hash($vertices[0])]; 239 | $v2 = $map[\spl_object_hash($vertices[1])]; 240 | 241 | // recreate edge and assign attributes 242 | if ($originalEdge instanceof EdgeUndirected) { 243 | $this->createEdgeUndirected($v1, $v2, $originalEdge->getAttributes()); 244 | } else { 245 | $this->createEdgeDirected($v1, $v2, $originalEdge->getAttributes()); 246 | } 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/Vertex.php: -------------------------------------------------------------------------------- 1 | graph = $graph; 27 | $this->attributes = $attributes; 28 | 29 | $graph->addVertex($this); 30 | } 31 | 32 | /** 33 | * get graph this vertex is attached to 34 | * 35 | * @return Graph 36 | */ 37 | public function getGraph() 38 | { 39 | return $this->graph; 40 | } 41 | 42 | /** 43 | * add the given edge to list of connected edges (MUST NOT be called manually) 44 | * 45 | * @param Edge $edge 46 | * @return void 47 | * @internal 48 | * @see Graph::createEdgeUndirected() instead! 49 | */ 50 | public function addEdge(Edge $edge) 51 | { 52 | $this->edges[] = $edge; 53 | } 54 | 55 | /** 56 | * check whether this vertex has a direct edge to given $vertex 57 | * 58 | * @param Vertex $vertex 59 | * @return bool 60 | * @uses Edge::hasVertexTarget() 61 | */ 62 | public function hasEdgeTo(Vertex $vertex) 63 | { 64 | foreach ($this->edges as $edge) { 65 | if ($edge->isConnection($this, $vertex)) { 66 | return true; 67 | } 68 | } 69 | 70 | return false; 71 | } 72 | 73 | /** 74 | * check whether the given vertex has a direct edge to THIS vertex 75 | * 76 | * @param Vertex $vertex 77 | * @return bool 78 | * @uses Vertex::hasEdgeTo() 79 | */ 80 | public function hasEdgeFrom(Vertex $vertex) 81 | { 82 | return $vertex->hasEdgeTo($this); 83 | } 84 | 85 | /** 86 | * get list of ALL edges attached to this vertex 87 | * 88 | * @psalm-return list 89 | * @return Edge[] 90 | */ 91 | public function getEdges() 92 | { 93 | return $this->edges; 94 | } 95 | 96 | /** 97 | * get list of all outgoing edges attached to this vertex 98 | * 99 | * @psalm-return list 100 | * @return Edge[] 101 | */ 102 | public function getEdgesOut() 103 | { 104 | $that = $this; 105 | $prev = null; 106 | 107 | return \array_values(\array_filter($this->edges, function (Edge $edge) use ($that, &$prev) { 108 | $ret = $edge->hasVertexStart($that); 109 | 110 | // skip duplicate directed loop edges 111 | if ($edge === $prev && $edge instanceof EdgeDirected) { 112 | $ret = false; 113 | } 114 | $prev = $edge; 115 | 116 | return $ret; 117 | })); 118 | } 119 | 120 | /** 121 | * get list of all ingoing edges attached to this vertex 122 | * 123 | * @psalm-return list 124 | * @return Edge[] 125 | */ 126 | public function getEdgesIn() 127 | { 128 | $that = $this; 129 | $prev = null; 130 | 131 | return \array_values(\array_filter($this->edges, function (Edge $edge) use ($that, &$prev) { 132 | $ret = $edge->hasVertexTarget($that); 133 | 134 | // skip duplicate directed loop edges 135 | if ($edge === $prev && $edge instanceof EdgeDirected) { 136 | $ret = false; 137 | } 138 | $prev = $edge; 139 | 140 | return $ret; 141 | })); 142 | } 143 | 144 | /** 145 | * get list of edges FROM this vertex TO the given vertex 146 | * 147 | * @param Vertex $vertex 148 | * @psalm-return list 149 | * @return Edge[] 150 | * @uses Edge::hasVertexTarget() 151 | */ 152 | public function getEdgesTo(Vertex $vertex) 153 | { 154 | $that = $this; 155 | 156 | return \array_values(\array_filter($this->edges, function (Edge $edge) use ($that, $vertex) { 157 | return $edge->isConnection($that, $vertex); 158 | })); 159 | } 160 | 161 | /** 162 | * get list of edges FROM the given vertex TO this vertex 163 | * 164 | * @param Vertex $vertex 165 | * @psalm-return list 166 | * @return Edge[] 167 | * @uses Vertex::getEdgesTo() 168 | */ 169 | public function getEdgesFrom(Vertex $vertex) 170 | { 171 | return $vertex->getEdgesTo($this); 172 | } 173 | 174 | /** 175 | * get list of adjacent vertices of this vertex (edge FROM or TO this vertex) 176 | * 177 | * If there are multiple parallel edges between the same Vertex, it will be 178 | * returned several times in the resulting list of vertices. 179 | * 180 | * @psalm-return list 181 | * @return Vertex[] 182 | * @uses Edge::hasVertexStart() 183 | * @uses Edge::getVerticesToFrom() 184 | * @uses Edge::getVerticesFromTo() 185 | */ 186 | public function getVerticesEdge() 187 | { 188 | $ret = array(); 189 | foreach ($this->edges as $edge) { 190 | if ($edge->hasVertexStart($this)) { 191 | $ret []= $edge->getVertexToFrom($this); 192 | } else { 193 | $ret []= $edge->getVertexFromTo($this); 194 | } 195 | } 196 | 197 | return $ret; 198 | } 199 | 200 | /** 201 | * get list of all vertices this vertex has an edge to 202 | * 203 | * If there are multiple parallel edges to the same Vertex, it will be 204 | * returned several times in the resulting list of vertices. 205 | * 206 | * @psalm-return list 207 | * @return Vertex[] 208 | * @uses Vertex::getEdgesOut() 209 | * @uses Edge::getVerticesToFrom() 210 | */ 211 | public function getVerticesEdgeTo() 212 | { 213 | $ret = array(); 214 | foreach ($this->getEdgesOut() as $edge) { 215 | $ret []= $edge->getVertexToFrom($this); 216 | } 217 | 218 | return $ret; 219 | } 220 | 221 | /** 222 | * get list of all vertices that have an edge TO this vertex 223 | * 224 | * If there are multiple parallel edges from the same Vertex, it will be 225 | * returned several times in the resulting list of vertices. 226 | * 227 | * @psalm-return list 228 | * @return Vertex[] 229 | * @uses Vertex::getEdgesIn() 230 | * @uses Edge::getVerticesFromTo() 231 | */ 232 | public function getVerticesEdgeFrom() 233 | { 234 | $ret = array(); 235 | foreach ($this->getEdgesIn() as $edge) { 236 | $ret []= $edge->getVertexFromTo($this); 237 | } 238 | 239 | return $ret; 240 | } 241 | 242 | /** 243 | * do NOT allow cloning of objects 244 | * 245 | * @throws \BadMethodCallException 246 | * @codeCoverageIgnore 247 | */ 248 | private function __clone() 249 | { 250 | throw new \BadMethodCallException(); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/Walk.php: -------------------------------------------------------------------------------- 1 | getVertexToFrom($vertexCurrent); 30 | $vertices []= $vertexCurrent; 31 | } 32 | 33 | return new self($vertices, \array_values($edges)); 34 | } 35 | 36 | /** 37 | * create new walk instance between given array of Vertex instances 38 | * 39 | * @param Vertex[] $vertices 40 | * @param null|string|callable(Edge):number $orderBy 41 | * @param bool $desc 42 | * @return Walk 43 | * @throws \UnderflowException if no vertices were given 44 | * @see self::factoryCycleFromVertices() for parameters $orderBy and $desc 45 | */ 46 | public static function factoryFromVertices(array $vertices, $orderBy = null, $desc = false) 47 | { 48 | $edges = array(); 49 | $last = NULL; 50 | foreach ($vertices as $vertex) { 51 | // skip first vertex as last is unknown 52 | if ($last !== NULL) { 53 | // pick edge between last vertex and this vertex 54 | \assert($last instanceof Vertex); 55 | $edges[] = self::pickEdge($last->getEdgesTo($vertex), $orderBy, $desc); 56 | } 57 | $last = $vertex; 58 | } 59 | if ($last === NULL) { 60 | throw new \UnderflowException('No vertices given'); 61 | } 62 | 63 | return new self(\array_values($vertices), $edges); 64 | } 65 | 66 | /** 67 | * create new cycle instance with edges between given vertices 68 | * 69 | * @param Vertex[] $vertices 70 | * @param null|string|callable(Edge):number $orderBy 71 | * @param bool $desc 72 | * @return Walk 73 | * @throws \UnderflowException if no vertices were given 74 | * @throws \InvalidArgumentException if vertices do not form a valid cycle 75 | * @see self::factoryCycleFromVertices() for parameters $orderBy and $desc 76 | * @uses self::factoryFromVertices() 77 | */ 78 | public static function factoryCycleFromVertices(array $vertices, $orderBy = null, $desc = false) 79 | { 80 | $cycle = self::factoryFromVertices($vertices, $orderBy, $desc); 81 | 82 | if (!$cycle->getEdges()) { 83 | throw new \InvalidArgumentException('Cycle with no edges can not exist'); 84 | } 85 | 86 | if (\reset($vertices) !== \end($vertices)) { 87 | throw new \InvalidArgumentException('Cycle has to start and end at the same vertex'); 88 | } 89 | 90 | return $cycle; 91 | } 92 | 93 | /** 94 | * create new cycle instance with vertices connected by given edges 95 | * 96 | * @param Edge[] $edges 97 | * @param Vertex $startVertex 98 | * @return Walk 99 | * @throws \InvalidArgumentException if the given array of edges does not represent a valid cycle 100 | * @uses self::factoryFromEdges() 101 | */ 102 | public static function factoryCycleFromEdges(array $edges, Vertex $startVertex) 103 | { 104 | $cycle = self::factoryFromEdges($edges, $startVertex); 105 | 106 | // ensure this walk is actually a cycle by checking start = end 107 | $vertices = $cycle->getVertices(); 108 | if (\end($vertices) !== $startVertex) { 109 | throw new \InvalidArgumentException('The given array of edges does not represent a cycle'); 110 | } 111 | 112 | return $cycle; 113 | } 114 | 115 | /** 116 | * @param Edge[] $edges 117 | * @param null|string|callable(Edge):number $orderBy 118 | * @param bool $desc 119 | * @return Edge 120 | * @throws \UnderflowException 121 | */ 122 | private static function pickEdge(array $edges, $orderBy, $desc) 123 | { 124 | if (!$edges) { 125 | throw new \UnderflowException('No edges between two vertices found'); 126 | } 127 | 128 | if ($orderBy === null) { 129 | return \reset($edges); 130 | } 131 | 132 | if (\is_string($orderBy)) { 133 | $orderBy = function (Edge $edge) use ($orderBy) { 134 | return $edge->getAttribute($orderBy); 135 | }; 136 | } 137 | 138 | $ret = NULL; 139 | $best = NULL; 140 | foreach ($edges as $edge) { 141 | $now = $orderBy($edge); 142 | 143 | if ($ret === NULL || ($desc && $now > $best) || (!$desc && $now < $best)) { 144 | $ret = $edge; 145 | $best = $now; 146 | } 147 | } 148 | 149 | return $ret; 150 | } 151 | 152 | /** 153 | * @var Vertex[] 154 | */ 155 | protected $vertices; 156 | 157 | /** 158 | * @var Edge[] 159 | */ 160 | protected $edges; 161 | 162 | /** 163 | * @psalm-param list $vertices 164 | * @psalm-param list $edges 165 | * @param Vertex[] $vertices 166 | * @param Edge[] $edges 167 | */ 168 | protected function __construct(array $vertices, array $edges) 169 | { 170 | $this->vertices = $vertices; 171 | $this->edges = $edges; 172 | } 173 | 174 | /** 175 | * return original graph 176 | * 177 | * @return Graph 178 | * @uses self::getVertices() 179 | * @uses Vertices::getVertexFirst() 180 | * @uses Vertex::getGraph() 181 | */ 182 | public function getGraph() 183 | { 184 | $vertex = \reset($this->vertices); 185 | \assert($vertex instanceof Vertex); 186 | 187 | return $vertex->getGraph(); 188 | } 189 | 190 | /** 191 | * return list of all edges of walk (in sequence visited in walk, may contain duplicates) 192 | * 193 | * @psalm-return list 194 | * @return Edge[] 195 | */ 196 | public function getEdges() 197 | { 198 | return $this->edges; 199 | } 200 | 201 | /** 202 | * return list of all vertices of walk (in sequence visited in walk, may contain duplicates) 203 | * 204 | * If you need to return the source vertex (first vertex of walk), you can 205 | * use something like this: 206 | * 207 | * ```php 208 | * $vertices = $walk->getVertices(); 209 | * $firstVertex = \reset($vertices); 210 | * ``` 211 | * 212 | * If you need to return the target/destination vertex (last vertex of walk), 213 | * you can use something like this: 214 | * 215 | * ```php 216 | * $vertices = $walk->getVertices(); 217 | * $lastVertex = \end($vertices); 218 | * ``` 219 | * 220 | * @psalm-return list 221 | * @return Vertex[] 222 | */ 223 | public function getVertices() 224 | { 225 | return $this->vertices; 226 | } 227 | 228 | /** 229 | * get alternating sequence of vertex, edge, vertex, edge, ..., vertex 230 | * 231 | * @psalm-return list 232 | * @return array 233 | */ 234 | public function getAlternatingSequence() 235 | { 236 | $ret = array(); 237 | for ($i = 0, $l = \count($this->edges); $i < $l; ++$i) { 238 | $ret []= $this->vertices[$i]; 239 | $ret []= $this->edges[$i]; 240 | } 241 | $ret[] = $this->vertices[$i]; 242 | 243 | return $ret; 244 | } 245 | } 246 | --------------------------------------------------------------------------------