├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── doc ├── base.dot.png ├── base.gv.png └── readme.dot └── src └── Gliph ├── Algorithm └── ConnectedComponent.php ├── Exception ├── IncompatibleGraphTypeException.php ├── InvalidVertexTypeException.php ├── NonexistentVertexException.php ├── OutOfRangeException.php ├── RuntimeException.php └── WrongVisitorStateException.php ├── Graph ├── AdjacencyList.php ├── Digraph.php ├── DirectedAdjacencyList.php ├── Graph.php ├── MutableDigraph.php ├── MutableGraph.php ├── MutableVertexSet.php └── UndirectedAdjacencyList.php ├── Traversal └── DepthFirst.php └── Visitor ├── DepthFirstBasicVisitor.php ├── DepthFirstNoOpVisitor.php ├── DepthFirstToposortVisitor.php ├── DepthFirstVisitorInterface.php ├── SimpleStatefulDepthFirstVisitor.php ├── StatefulDepthFirstVisitor.php ├── StatefulVisitorInterface.php └── TarjanSCCVisitor.php /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Sam Boyer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gliph 2 | 3 | [![Build Status](https://travis-ci.org/sdboyer/gliph.png?branch=master)](https://travis-ci.org/sdboyer/gliph) 4 | [![Latest Stable Version](https://poser.pugx.org/sdboyer/gliph/v/stable.png)](https://packagist.org/packages/sdboyer/gliph) 5 | [![Coverage Status](https://coveralls.io/repos/sdboyer/gliph/badge.png?branch=master)](https://coveralls.io/r/sdboyer/gliph?branch=master) 6 | 7 | Gliph is a graph library for PHP. It provides graph building blocks and datastructures for use by other PHP applications. It is designed for use with in-memory graphs, not for interaction with a graph database like [Cayley](https://github.com/google/cayley) or [Neo4J](http://neo4j.org/) (though it could be used to facilitate such connection). 8 | 9 | Gliph aims for both sane interfaces and as performant an implementation as userspace PHP allows. 10 | 11 | This does require knowing enough about graphs to know what type is appropriate for your use case, but we are aiming to provide helpers that simplify those choices. 12 | 13 | ## Quickstart 14 | 15 | Working with gliph is easy: pick a graph implementation, then add edges and vertices as needed. ()Note that gliph currently supports only object vertices, though this limitation may be loosened in future releases) 16 | 17 | ```php 18 | val = $val; 27 | } 28 | } 29 | 30 | $vertices = array( 31 | 'a' => new Vertex('a'), 32 | 'b' => new Vertex('b'), 33 | 'c' => new Vertex('c'), 34 | 'd' => new Vertex('d'), 35 | 'e' => new Vertex('e'), 36 | 'f' => new Vertex('f'), 37 | ); 38 | 39 | $g = new DirectedAdjacencyList(); 40 | 41 | foreach ($vertices as $vertex) { 42 | $g->ensureVertex($vertex); 43 | } 44 | 45 | $g->ensureArc($vertices['a'], $vertices['b']); 46 | $g->ensureArc($vertices['b'], $vertices['c']); 47 | $g->ensureArc($vertices['a'], $vertices['c']); 48 | $g->ensureArc($vertices['d'], $vertices['a']); 49 | $g->ensureArc($vertices['d'], $vertices['e']); 50 | ``` 51 | 52 | This would create the following directed graph: 53 | 54 | ![Base digraph](doc/base.dot.png) 55 | 56 | Once your graph is created, gliph provides a number of `Traversable` mechanisms for doing work on the graph. These mechanisms are important to understand and are explored in greater detail [in the wiki](https://github.com/sdboyer/gliph/wiki/Iterators), but for those familiar with graph theory, the method naming is intended to be self-explanatory: 57 | 58 | For directed and undirected graphs: 59 | 60 | * `Graph::vertices()` 61 | * `Graph::edges()` 62 | * `Graph::adjacentTo($vertex)` 63 | * `Graph::incidentTo($vertex)` 64 | 65 | And for directed graphs only: 66 | 67 | * `Digraph::successorsOf($vertex)` 68 | * `Digraph::predecessorsOf($vertex)` 69 | * `Digraph::arcsFrom($vertex)` 70 | * `Digraph::arcsTo($vertex)` 71 | 72 | ## Core Concepts 73 | 74 | Gliph has several conceptual components that work together to create a coherent library: graph implementations, algorithms, and visitors. 75 | 76 | Gliph has several components that work together: graph classes, algorithms, and visitors. Generally speaking, Gliph is patterned after the [C++ Boost Graph Library](http://www.boost.org/libs/graph/doc); reading their documentation can yield a lot of insight into how Gliph is intended to work. 77 | 78 | ### Graphs 79 | 80 | Gliph’s most important baseline export is its assorted [graph interfaces](https://github.com/sdboyer/gliph/tree/master/src/Gliph/Graph). The other components (algorithms and visitors) rely **strictly** on the interfaces, never on concrete implementations. Consequently, users of gliph can craft case-specific implementations with the assurance that they will work with the algorithms. To assist towards that end, gliph provides phpunit testing traits that make it easy to ensure your custom graph implementations conform to both the letter and the spirit of gliph’s interfaces. 81 | 82 | Gliph’s own implementations are designed to be as performant as possible for the general case. Current implementations include only adjacency lists, in both directed and undirected flavors. 83 | 84 | ### Algorithms 85 | 86 | Gliph provides various algorithms that can be run on graph objects. These algorithms interact with the graph by making calls to methods, primarily the iterators, defined in the assorted Graph interfaces. If a graph implements the interface type-hinted by a particular algorithm, then the algorithm can run on that graph. But the efficiency of the algorithm will be largely determined by how efficiently that graph implementation can meet the requirements of the interface. Adjacency lists, for example, are not terribly efficient at providing a list of all edges in a graph, but are very good at single-vertex-centric operations. 87 | 88 | Gliph's algorithms are typically implemented quite sparsely (especially traversers) - they seek to implement the simplest, most generic version of an algorithm. They also may not return any output, as that work is left to Visitors. 89 | 90 | ### Visitors 91 | 92 | Most algorithms require a visitor object to be provided. The visitor conforms to an interface specified by the algorithm, and the algorithm will call the visitor at certain choice points during its execution. This allows the algorithms to stay highly generic, while visitors can be tailored to a more specific purpose. 93 | 94 | For example, a ```DepthFirst``` visitor might be used to calculate vertex reach, or generate a topologically sorted list. Each of these are things that a depth-first graph traversal can do. But the work of doing so is left to the visitor so that only one traversal algorithm is needed, and that algorithm is as cheap (memory and cycles) as possible. 95 | 96 | ## Acknowledgements 97 | 98 | This library draws inspiration from the [C++ Boost Graph Library](http://www.boost.org/libs/graph/doc), though has diverged in some significant design philosophies. Gliph generally follows the same patterns as [gogl](https://github.com/sdboyer/gogl), though the concepts are more rigorously applied there. 99 | 100 | ## License 101 | 102 | MIT 103 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sdboyer/gliph", 3 | "description": "A graph library for PHP.", 4 | "license": "MIT", 5 | "keywords": ["gliph", "library", "php", "spl", "graph"], 6 | "homepage": "http://github.com/sdboyer/gliph", 7 | "type": "library", 8 | "authors": [ 9 | { 10 | "name": "Sam Boyer", 11 | "email": "tech@samboyer.org" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=5.5" 16 | }, 17 | "require-dev": { 18 | "satooshi/php-coveralls": "0.6.*", 19 | "phpunit/phpunit": "3.7.*" 20 | }, 21 | "autoload": { 22 | "psr-0": { 23 | "Gliph\\": ["src/", "tests/"] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "511e20efc88e6c7bed0524158221a37f", 8 | "packages": [], 9 | "packages-dev": [ 10 | { 11 | "name": "guzzle/guzzle", 12 | "version": "v3.9.2", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/guzzle/guzzle3.git", 16 | "reference": "54991459675c1a2924122afbb0e5609ade581155" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/guzzle/guzzle3/zipball/54991459675c1a2924122afbb0e5609ade581155", 21 | "reference": "54991459675c1a2924122afbb0e5609ade581155", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "ext-curl": "*", 26 | "php": ">=5.3.3", 27 | "symfony/event-dispatcher": "~2.1" 28 | }, 29 | "replace": { 30 | "guzzle/batch": "self.version", 31 | "guzzle/cache": "self.version", 32 | "guzzle/common": "self.version", 33 | "guzzle/http": "self.version", 34 | "guzzle/inflection": "self.version", 35 | "guzzle/iterator": "self.version", 36 | "guzzle/log": "self.version", 37 | "guzzle/parser": "self.version", 38 | "guzzle/plugin": "self.version", 39 | "guzzle/plugin-async": "self.version", 40 | "guzzle/plugin-backoff": "self.version", 41 | "guzzle/plugin-cache": "self.version", 42 | "guzzle/plugin-cookie": "self.version", 43 | "guzzle/plugin-curlauth": "self.version", 44 | "guzzle/plugin-error-response": "self.version", 45 | "guzzle/plugin-history": "self.version", 46 | "guzzle/plugin-log": "self.version", 47 | "guzzle/plugin-md5": "self.version", 48 | "guzzle/plugin-mock": "self.version", 49 | "guzzle/plugin-oauth": "self.version", 50 | "guzzle/service": "self.version", 51 | "guzzle/stream": "self.version" 52 | }, 53 | "require-dev": { 54 | "doctrine/cache": "~1.3", 55 | "monolog/monolog": "~1.0", 56 | "phpunit/phpunit": "3.7.*", 57 | "psr/log": "~1.0", 58 | "symfony/class-loader": "~2.1", 59 | "zendframework/zend-cache": "2.*,<2.3", 60 | "zendframework/zend-log": "2.*,<2.3" 61 | }, 62 | "type": "library", 63 | "extra": { 64 | "branch-alias": { 65 | "dev-master": "3.9-dev" 66 | } 67 | }, 68 | "autoload": { 69 | "psr-0": { 70 | "Guzzle": "src/", 71 | "Guzzle\\Tests": "tests/" 72 | } 73 | }, 74 | "notification-url": "https://packagist.org/downloads/", 75 | "license": [ 76 | "MIT" 77 | ], 78 | "authors": [ 79 | { 80 | "name": "Michael Dowling", 81 | "email": "mtdowling@gmail.com", 82 | "homepage": "https://github.com/mtdowling" 83 | }, 84 | { 85 | "name": "Guzzle Community", 86 | "homepage": "https://github.com/guzzle/guzzle/contributors" 87 | } 88 | ], 89 | "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients", 90 | "homepage": "http://guzzlephp.org/", 91 | "keywords": [ 92 | "client", 93 | "curl", 94 | "framework", 95 | "http", 96 | "http client", 97 | "rest", 98 | "web service" 99 | ], 100 | "time": "2014-08-11 04:32:36" 101 | }, 102 | { 103 | "name": "phpunit/php-code-coverage", 104 | "version": "1.2.18", 105 | "source": { 106 | "type": "git", 107 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 108 | "reference": "fe2466802556d3fe4e4d1d58ffd3ccfd0a19be0b" 109 | }, 110 | "dist": { 111 | "type": "zip", 112 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/fe2466802556d3fe4e4d1d58ffd3ccfd0a19be0b", 113 | "reference": "fe2466802556d3fe4e4d1d58ffd3ccfd0a19be0b", 114 | "shasum": "" 115 | }, 116 | "require": { 117 | "php": ">=5.3.3", 118 | "phpunit/php-file-iterator": ">=1.3.0@stable", 119 | "phpunit/php-text-template": ">=1.2.0@stable", 120 | "phpunit/php-token-stream": ">=1.1.3,<1.3.0" 121 | }, 122 | "require-dev": { 123 | "phpunit/phpunit": "3.7.*@dev" 124 | }, 125 | "suggest": { 126 | "ext-dom": "*", 127 | "ext-xdebug": ">=2.0.5" 128 | }, 129 | "type": "library", 130 | "extra": { 131 | "branch-alias": { 132 | "dev-master": "1.2.x-dev" 133 | } 134 | }, 135 | "autoload": { 136 | "classmap": [ 137 | "PHP/" 138 | ] 139 | }, 140 | "notification-url": "https://packagist.org/downloads/", 141 | "include-path": [ 142 | "" 143 | ], 144 | "license": [ 145 | "BSD-3-Clause" 146 | ], 147 | "authors": [ 148 | { 149 | "name": "Sebastian Bergmann", 150 | "email": "sb@sebastian-bergmann.de", 151 | "role": "lead" 152 | } 153 | ], 154 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 155 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 156 | "keywords": [ 157 | "coverage", 158 | "testing", 159 | "xunit" 160 | ], 161 | "time": "2014-09-02 10:13:14" 162 | }, 163 | { 164 | "name": "phpunit/php-file-iterator", 165 | "version": "1.3.4", 166 | "source": { 167 | "type": "git", 168 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 169 | "reference": "acd690379117b042d1c8af1fafd61bde001bf6bb" 170 | }, 171 | "dist": { 172 | "type": "zip", 173 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/acd690379117b042d1c8af1fafd61bde001bf6bb", 174 | "reference": "acd690379117b042d1c8af1fafd61bde001bf6bb", 175 | "shasum": "" 176 | }, 177 | "require": { 178 | "php": ">=5.3.3" 179 | }, 180 | "type": "library", 181 | "autoload": { 182 | "classmap": [ 183 | "File/" 184 | ] 185 | }, 186 | "notification-url": "https://packagist.org/downloads/", 187 | "include-path": [ 188 | "" 189 | ], 190 | "license": [ 191 | "BSD-3-Clause" 192 | ], 193 | "authors": [ 194 | { 195 | "name": "Sebastian Bergmann", 196 | "email": "sb@sebastian-bergmann.de", 197 | "role": "lead" 198 | } 199 | ], 200 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 201 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 202 | "keywords": [ 203 | "filesystem", 204 | "iterator" 205 | ], 206 | "time": "2013-10-10 15:34:57" 207 | }, 208 | { 209 | "name": "phpunit/php-text-template", 210 | "version": "1.2.0", 211 | "source": { 212 | "type": "git", 213 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 214 | "reference": "206dfefc0ffe9cebf65c413e3d0e809c82fbf00a" 215 | }, 216 | "dist": { 217 | "type": "zip", 218 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/206dfefc0ffe9cebf65c413e3d0e809c82fbf00a", 219 | "reference": "206dfefc0ffe9cebf65c413e3d0e809c82fbf00a", 220 | "shasum": "" 221 | }, 222 | "require": { 223 | "php": ">=5.3.3" 224 | }, 225 | "type": "library", 226 | "autoload": { 227 | "classmap": [ 228 | "Text/" 229 | ] 230 | }, 231 | "notification-url": "https://packagist.org/downloads/", 232 | "include-path": [ 233 | "" 234 | ], 235 | "license": [ 236 | "BSD-3-Clause" 237 | ], 238 | "authors": [ 239 | { 240 | "name": "Sebastian Bergmann", 241 | "email": "sb@sebastian-bergmann.de", 242 | "role": "lead" 243 | } 244 | ], 245 | "description": "Simple template engine.", 246 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 247 | "keywords": [ 248 | "template" 249 | ], 250 | "time": "2014-01-30 17:20:04" 251 | }, 252 | { 253 | "name": "phpunit/php-timer", 254 | "version": "1.0.5", 255 | "source": { 256 | "type": "git", 257 | "url": "https://github.com/sebastianbergmann/php-timer.git", 258 | "reference": "1.0.5" 259 | }, 260 | "dist": { 261 | "type": "zip", 262 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1.0.5", 263 | "reference": "1.0.5", 264 | "shasum": "" 265 | }, 266 | "require": { 267 | "php": ">=5.3.3" 268 | }, 269 | "type": "library", 270 | "autoload": { 271 | "classmap": [ 272 | "PHP/" 273 | ] 274 | }, 275 | "notification-url": "https://packagist.org/downloads/", 276 | "include-path": [ 277 | "" 278 | ], 279 | "license": [ 280 | "BSD-3-Clause" 281 | ], 282 | "authors": [ 283 | { 284 | "name": "Sebastian Bergmann", 285 | "email": "sb@sebastian-bergmann.de", 286 | "role": "lead" 287 | } 288 | ], 289 | "description": "Utility class for timing", 290 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 291 | "keywords": [ 292 | "timer" 293 | ], 294 | "time": "2013-08-02 07:42:54" 295 | }, 296 | { 297 | "name": "phpunit/php-token-stream", 298 | "version": "1.2.2", 299 | "source": { 300 | "type": "git", 301 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 302 | "reference": "ad4e1e23ae01b483c16f600ff1bebec184588e32" 303 | }, 304 | "dist": { 305 | "type": "zip", 306 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/ad4e1e23ae01b483c16f600ff1bebec184588e32", 307 | "reference": "ad4e1e23ae01b483c16f600ff1bebec184588e32", 308 | "shasum": "" 309 | }, 310 | "require": { 311 | "ext-tokenizer": "*", 312 | "php": ">=5.3.3" 313 | }, 314 | "type": "library", 315 | "extra": { 316 | "branch-alias": { 317 | "dev-master": "1.2-dev" 318 | } 319 | }, 320 | "autoload": { 321 | "classmap": [ 322 | "PHP/" 323 | ] 324 | }, 325 | "notification-url": "https://packagist.org/downloads/", 326 | "include-path": [ 327 | "" 328 | ], 329 | "license": [ 330 | "BSD-3-Clause" 331 | ], 332 | "authors": [ 333 | { 334 | "name": "Sebastian Bergmann", 335 | "email": "sb@sebastian-bergmann.de", 336 | "role": "lead" 337 | } 338 | ], 339 | "description": "Wrapper around PHP's tokenizer extension.", 340 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 341 | "keywords": [ 342 | "tokenizer" 343 | ], 344 | "time": "2014-03-03 05:10:30" 345 | }, 346 | { 347 | "name": "phpunit/phpunit", 348 | "version": "3.7.37", 349 | "source": { 350 | "type": "git", 351 | "url": "https://github.com/sebastianbergmann/phpunit.git", 352 | "reference": "ae6cefd7cc84586a5ef27e04bae11ee940ec63dc" 353 | }, 354 | "dist": { 355 | "type": "zip", 356 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ae6cefd7cc84586a5ef27e04bae11ee940ec63dc", 357 | "reference": "ae6cefd7cc84586a5ef27e04bae11ee940ec63dc", 358 | "shasum": "" 359 | }, 360 | "require": { 361 | "ext-ctype": "*", 362 | "ext-dom": "*", 363 | "ext-json": "*", 364 | "ext-pcre": "*", 365 | "ext-reflection": "*", 366 | "ext-spl": "*", 367 | "php": ">=5.3.3", 368 | "phpunit/php-code-coverage": "~1.2", 369 | "phpunit/php-file-iterator": "~1.3", 370 | "phpunit/php-text-template": "~1.1", 371 | "phpunit/php-timer": "~1.0", 372 | "phpunit/phpunit-mock-objects": "~1.2", 373 | "symfony/yaml": "~2.0" 374 | }, 375 | "require-dev": { 376 | "pear-pear.php.net/pear": "1.9.4" 377 | }, 378 | "suggest": { 379 | "phpunit/php-invoker": "~1.1" 380 | }, 381 | "bin": [ 382 | "composer/bin/phpunit" 383 | ], 384 | "type": "library", 385 | "extra": { 386 | "branch-alias": { 387 | "dev-master": "3.7.x-dev" 388 | } 389 | }, 390 | "autoload": { 391 | "classmap": [ 392 | "PHPUnit/" 393 | ] 394 | }, 395 | "notification-url": "https://packagist.org/downloads/", 396 | "include-path": [ 397 | "", 398 | "../../symfony/yaml/" 399 | ], 400 | "license": [ 401 | "BSD-3-Clause" 402 | ], 403 | "authors": [ 404 | { 405 | "name": "Sebastian Bergmann", 406 | "email": "sebastian@phpunit.de", 407 | "role": "lead" 408 | } 409 | ], 410 | "description": "The PHP Unit Testing framework.", 411 | "homepage": "http://www.phpunit.de/", 412 | "keywords": [ 413 | "phpunit", 414 | "testing", 415 | "xunit" 416 | ], 417 | "time": "2014-04-30 12:24:19" 418 | }, 419 | { 420 | "name": "phpunit/phpunit-mock-objects", 421 | "version": "1.2.3", 422 | "source": { 423 | "type": "git", 424 | "url": "git://github.com/sebastianbergmann/phpunit-mock-objects.git", 425 | "reference": "1.2.3" 426 | }, 427 | "dist": { 428 | "type": "zip", 429 | "url": "https://github.com/sebastianbergmann/phpunit-mock-objects/archive/1.2.3.zip", 430 | "reference": "1.2.3", 431 | "shasum": "" 432 | }, 433 | "require": { 434 | "php": ">=5.3.3", 435 | "phpunit/php-text-template": ">=1.1.1@stable" 436 | }, 437 | "suggest": { 438 | "ext-soap": "*" 439 | }, 440 | "type": "library", 441 | "autoload": { 442 | "classmap": [ 443 | "PHPUnit/" 444 | ] 445 | }, 446 | "notification-url": "https://packagist.org/downloads/", 447 | "include-path": [ 448 | "" 449 | ], 450 | "license": [ 451 | "BSD-3-Clause" 452 | ], 453 | "authors": [ 454 | { 455 | "name": "Sebastian Bergmann", 456 | "email": "sb@sebastian-bergmann.de", 457 | "role": "lead" 458 | } 459 | ], 460 | "description": "Mock Object library for PHPUnit", 461 | "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", 462 | "keywords": [ 463 | "mock", 464 | "xunit" 465 | ], 466 | "time": "2013-01-13 10:24:48" 467 | }, 468 | { 469 | "name": "psr/log", 470 | "version": "1.0.0", 471 | "source": { 472 | "type": "git", 473 | "url": "https://github.com/php-fig/log.git", 474 | "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b" 475 | }, 476 | "dist": { 477 | "type": "zip", 478 | "url": "https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278b", 479 | "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b", 480 | "shasum": "" 481 | }, 482 | "type": "library", 483 | "autoload": { 484 | "psr-0": { 485 | "Psr\\Log\\": "" 486 | } 487 | }, 488 | "notification-url": "https://packagist.org/downloads/", 489 | "license": [ 490 | "MIT" 491 | ], 492 | "authors": [ 493 | { 494 | "name": "PHP-FIG", 495 | "homepage": "http://www.php-fig.org/" 496 | } 497 | ], 498 | "description": "Common interface for logging libraries", 499 | "keywords": [ 500 | "log", 501 | "psr", 502 | "psr-3" 503 | ], 504 | "time": "2012-12-21 11:40:51" 505 | }, 506 | { 507 | "name": "satooshi/php-coveralls", 508 | "version": "v0.6.1", 509 | "source": { 510 | "type": "git", 511 | "url": "https://github.com/satooshi/php-coveralls.git", 512 | "reference": "dd0df95bd37a7cf5c5c50304dfe260ffe4b50760" 513 | }, 514 | "dist": { 515 | "type": "zip", 516 | "url": "https://api.github.com/repos/satooshi/php-coveralls/zipball/dd0df95bd37a7cf5c5c50304dfe260ffe4b50760", 517 | "reference": "dd0df95bd37a7cf5c5c50304dfe260ffe4b50760", 518 | "shasum": "" 519 | }, 520 | "require": { 521 | "ext-curl": "*", 522 | "ext-json": "*", 523 | "ext-simplexml": "*", 524 | "guzzle/guzzle": ">=3.0", 525 | "php": ">=5.3", 526 | "psr/log": "1.0.0", 527 | "symfony/config": ">=2.0", 528 | "symfony/console": ">=2.0", 529 | "symfony/stopwatch": ">=2.2", 530 | "symfony/yaml": ">=2.0" 531 | }, 532 | "require-dev": { 533 | "apigen/apigen": "2.8.*@stable", 534 | "pdepend/pdepend": "dev-master", 535 | "phpmd/phpmd": "dev-master", 536 | "phpunit/php-invoker": ">=1.1.0,<1.2.0", 537 | "phpunit/phpunit": "3.7.*@stable", 538 | "sebastian/finder-facade": "dev-master", 539 | "sebastian/phpcpd": "1.4.*@stable", 540 | "squizlabs/php_codesniffer": "1.4.*@stable", 541 | "theseer/fdomdocument": "dev-master" 542 | }, 543 | "bin": [ 544 | "composer/bin/coveralls" 545 | ], 546 | "type": "library", 547 | "autoload": { 548 | "psr-0": { 549 | "Contrib\\Component": "src/", 550 | "Contrib\\Bundle": "src/" 551 | } 552 | }, 553 | "notification-url": "https://packagist.org/downloads/", 554 | "license": [ 555 | "MIT" 556 | ], 557 | "authors": [ 558 | { 559 | "name": "Kitamura Satoshi", 560 | "email": "with.no.parachute@gmail.com", 561 | "homepage": "https://www.facebook.com/satooshi.jp" 562 | } 563 | ], 564 | "description": "PHP client library for Coveralls API", 565 | "homepage": "https://github.com/satooshi/php-coveralls", 566 | "keywords": [ 567 | "ci", 568 | "coverage", 569 | "github", 570 | "test" 571 | ], 572 | "time": "2013-05-04 08:07:33" 573 | }, 574 | { 575 | "name": "symfony/config", 576 | "version": "v2.5.4", 577 | "target-dir": "Symfony/Component/Config", 578 | "source": { 579 | "type": "git", 580 | "url": "https://github.com/symfony/Config.git", 581 | "reference": "080eabdc256c1d7a3a7cf6296271edb68eb1ab2b" 582 | }, 583 | "dist": { 584 | "type": "zip", 585 | "url": "https://api.github.com/repos/symfony/Config/zipball/080eabdc256c1d7a3a7cf6296271edb68eb1ab2b", 586 | "reference": "080eabdc256c1d7a3a7cf6296271edb68eb1ab2b", 587 | "shasum": "" 588 | }, 589 | "require": { 590 | "php": ">=5.3.3", 591 | "symfony/filesystem": "~2.3" 592 | }, 593 | "type": "library", 594 | "extra": { 595 | "branch-alias": { 596 | "dev-master": "2.5-dev" 597 | } 598 | }, 599 | "autoload": { 600 | "psr-0": { 601 | "Symfony\\Component\\Config\\": "" 602 | } 603 | }, 604 | "notification-url": "https://packagist.org/downloads/", 605 | "license": [ 606 | "MIT" 607 | ], 608 | "authors": [ 609 | { 610 | "name": "Symfony Community", 611 | "homepage": "http://symfony.com/contributors" 612 | }, 613 | { 614 | "name": "Fabien Potencier", 615 | "email": "fabien@symfony.com" 616 | } 617 | ], 618 | "description": "Symfony Config Component", 619 | "homepage": "http://symfony.com", 620 | "time": "2014-08-31 03:22:04" 621 | }, 622 | { 623 | "name": "symfony/console", 624 | "version": "v2.5.4", 625 | "target-dir": "Symfony/Component/Console", 626 | "source": { 627 | "type": "git", 628 | "url": "https://github.com/symfony/Console.git", 629 | "reference": "748beed2a1e73179c3f5154d33fe6ae100c1aeb1" 630 | }, 631 | "dist": { 632 | "type": "zip", 633 | "url": "https://api.github.com/repos/symfony/Console/zipball/748beed2a1e73179c3f5154d33fe6ae100c1aeb1", 634 | "reference": "748beed2a1e73179c3f5154d33fe6ae100c1aeb1", 635 | "shasum": "" 636 | }, 637 | "require": { 638 | "php": ">=5.3.3" 639 | }, 640 | "require-dev": { 641 | "psr/log": "~1.0", 642 | "symfony/event-dispatcher": "~2.1" 643 | }, 644 | "suggest": { 645 | "psr/log": "For using the console logger", 646 | "symfony/event-dispatcher": "" 647 | }, 648 | "type": "library", 649 | "extra": { 650 | "branch-alias": { 651 | "dev-master": "2.5-dev" 652 | } 653 | }, 654 | "autoload": { 655 | "psr-0": { 656 | "Symfony\\Component\\Console\\": "" 657 | } 658 | }, 659 | "notification-url": "https://packagist.org/downloads/", 660 | "license": [ 661 | "MIT" 662 | ], 663 | "authors": [ 664 | { 665 | "name": "Symfony Community", 666 | "homepage": "http://symfony.com/contributors" 667 | }, 668 | { 669 | "name": "Fabien Potencier", 670 | "email": "fabien@symfony.com" 671 | } 672 | ], 673 | "description": "Symfony Console Component", 674 | "homepage": "http://symfony.com", 675 | "time": "2014-08-14 16:10:54" 676 | }, 677 | { 678 | "name": "symfony/event-dispatcher", 679 | "version": "v2.5.4", 680 | "target-dir": "Symfony/Component/EventDispatcher", 681 | "source": { 682 | "type": "git", 683 | "url": "https://github.com/symfony/EventDispatcher.git", 684 | "reference": "8faf5cc7e80fde74a650a36e60d32ce3c3e0457b" 685 | }, 686 | "dist": { 687 | "type": "zip", 688 | "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/8faf5cc7e80fde74a650a36e60d32ce3c3e0457b", 689 | "reference": "8faf5cc7e80fde74a650a36e60d32ce3c3e0457b", 690 | "shasum": "" 691 | }, 692 | "require": { 693 | "php": ">=5.3.3" 694 | }, 695 | "require-dev": { 696 | "psr/log": "~1.0", 697 | "symfony/config": "~2.0", 698 | "symfony/dependency-injection": "~2.0", 699 | "symfony/stopwatch": "~2.2" 700 | }, 701 | "suggest": { 702 | "symfony/dependency-injection": "", 703 | "symfony/http-kernel": "" 704 | }, 705 | "type": "library", 706 | "extra": { 707 | "branch-alias": { 708 | "dev-master": "2.5-dev" 709 | } 710 | }, 711 | "autoload": { 712 | "psr-0": { 713 | "Symfony\\Component\\EventDispatcher\\": "" 714 | } 715 | }, 716 | "notification-url": "https://packagist.org/downloads/", 717 | "license": [ 718 | "MIT" 719 | ], 720 | "authors": [ 721 | { 722 | "name": "Symfony Community", 723 | "homepage": "http://symfony.com/contributors" 724 | }, 725 | { 726 | "name": "Fabien Potencier", 727 | "email": "fabien@symfony.com" 728 | } 729 | ], 730 | "description": "Symfony EventDispatcher Component", 731 | "homepage": "http://symfony.com", 732 | "time": "2014-07-28 13:20:46" 733 | }, 734 | { 735 | "name": "symfony/filesystem", 736 | "version": "v2.5.4", 737 | "target-dir": "Symfony/Component/Filesystem", 738 | "source": { 739 | "type": "git", 740 | "url": "https://github.com/symfony/Filesystem.git", 741 | "reference": "a765efd199e02ff4001c115c318e219030be9364" 742 | }, 743 | "dist": { 744 | "type": "zip", 745 | "url": "https://api.github.com/repos/symfony/Filesystem/zipball/a765efd199e02ff4001c115c318e219030be9364", 746 | "reference": "a765efd199e02ff4001c115c318e219030be9364", 747 | "shasum": "" 748 | }, 749 | "require": { 750 | "php": ">=5.3.3" 751 | }, 752 | "type": "library", 753 | "extra": { 754 | "branch-alias": { 755 | "dev-master": "2.5-dev" 756 | } 757 | }, 758 | "autoload": { 759 | "psr-0": { 760 | "Symfony\\Component\\Filesystem\\": "" 761 | } 762 | }, 763 | "notification-url": "https://packagist.org/downloads/", 764 | "license": [ 765 | "MIT" 766 | ], 767 | "authors": [ 768 | { 769 | "name": "Symfony Community", 770 | "homepage": "http://symfony.com/contributors" 771 | }, 772 | { 773 | "name": "Fabien Potencier", 774 | "email": "fabien@symfony.com" 775 | } 776 | ], 777 | "description": "Symfony Filesystem Component", 778 | "homepage": "http://symfony.com", 779 | "time": "2014-09-03 09:00:14" 780 | }, 781 | { 782 | "name": "symfony/stopwatch", 783 | "version": "v2.5.4", 784 | "target-dir": "Symfony/Component/Stopwatch", 785 | "source": { 786 | "type": "git", 787 | "url": "https://github.com/symfony/Stopwatch.git", 788 | "reference": "22ab4f76cdeefd38b00022a6be5709190a2fd046" 789 | }, 790 | "dist": { 791 | "type": "zip", 792 | "url": "https://api.github.com/repos/symfony/Stopwatch/zipball/22ab4f76cdeefd38b00022a6be5709190a2fd046", 793 | "reference": "22ab4f76cdeefd38b00022a6be5709190a2fd046", 794 | "shasum": "" 795 | }, 796 | "require": { 797 | "php": ">=5.3.3" 798 | }, 799 | "type": "library", 800 | "extra": { 801 | "branch-alias": { 802 | "dev-master": "2.5-dev" 803 | } 804 | }, 805 | "autoload": { 806 | "psr-0": { 807 | "Symfony\\Component\\Stopwatch\\": "" 808 | } 809 | }, 810 | "notification-url": "https://packagist.org/downloads/", 811 | "license": [ 812 | "MIT" 813 | ], 814 | "authors": [ 815 | { 816 | "name": "Symfony Community", 817 | "homepage": "http://symfony.com/contributors" 818 | }, 819 | { 820 | "name": "Fabien Potencier", 821 | "email": "fabien@symfony.com" 822 | } 823 | ], 824 | "description": "Symfony Stopwatch Component", 825 | "homepage": "http://symfony.com", 826 | "time": "2014-08-14 16:10:54" 827 | }, 828 | { 829 | "name": "symfony/yaml", 830 | "version": "v2.5.4", 831 | "target-dir": "Symfony/Component/Yaml", 832 | "source": { 833 | "type": "git", 834 | "url": "https://github.com/symfony/Yaml.git", 835 | "reference": "01a7695bcfb013d0a15c6757e15aae120342986f" 836 | }, 837 | "dist": { 838 | "type": "zip", 839 | "url": "https://api.github.com/repos/symfony/Yaml/zipball/01a7695bcfb013d0a15c6757e15aae120342986f", 840 | "reference": "01a7695bcfb013d0a15c6757e15aae120342986f", 841 | "shasum": "" 842 | }, 843 | "require": { 844 | "php": ">=5.3.3" 845 | }, 846 | "type": "library", 847 | "extra": { 848 | "branch-alias": { 849 | "dev-master": "2.5-dev" 850 | } 851 | }, 852 | "autoload": { 853 | "psr-0": { 854 | "Symfony\\Component\\Yaml\\": "" 855 | } 856 | }, 857 | "notification-url": "https://packagist.org/downloads/", 858 | "license": [ 859 | "MIT" 860 | ], 861 | "authors": [ 862 | { 863 | "name": "Symfony Community", 864 | "homepage": "http://symfony.com/contributors" 865 | }, 866 | { 867 | "name": "Fabien Potencier", 868 | "email": "fabien@symfony.com" 869 | } 870 | ], 871 | "description": "Symfony Yaml Component", 872 | "homepage": "http://symfony.com", 873 | "time": "2014-08-31 03:22:04" 874 | } 875 | ], 876 | "aliases": [], 877 | "minimum-stability": "stable", 878 | "stability-flags": [], 879 | "prefer-stable": false, 880 | "platform": { 881 | "php": ">=5.5" 882 | }, 883 | "platform-dev": [] 884 | } 885 | -------------------------------------------------------------------------------- /doc/base.dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdboyer/gliph/5ec6314b2b211053f6bae989b95446ccf6e8ded0/doc/base.dot.png -------------------------------------------------------------------------------- /doc/base.gv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdboyer/gliph/5ec6314b2b211053f6bae989b95446ccf6e8ded0/doc/base.gv.png -------------------------------------------------------------------------------- /doc/readme.dot: -------------------------------------------------------------------------------- 1 | digraph base { 2 | a -> b -> c; 3 | a -> c; 4 | d -> a; 5 | d -> e; 6 | f; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/Gliph/Algorithm/ConnectedComponent.php: -------------------------------------------------------------------------------- 1 | attach($vertex, $counter); 34 | $lowlimits->attach($vertex, $counter); 35 | $stack[] = $vertex; 36 | $counter++; 37 | 38 | foreach ($graph->successorsOf($vertex) as $head) { 39 | if (!$indices->contains($head)) { 40 | $visit($head); 41 | $lowlimits[$vertex] = min($lowlimits[$vertex], $lowlimits[$head]); 42 | } 43 | else if (in_array($head, $stack, TRUE)) { 44 | $lowlimits[$vertex] = min($lowlimits[$vertex], $indices[$head]); 45 | } 46 | } 47 | 48 | if ($lowlimits[$vertex] === $indices[$vertex]) { 49 | $visitor->newComponent(); 50 | do { 51 | $other = array_pop($stack); 52 | $visitor->addToCurrentComponent($other); 53 | } while ($other != $vertex); 54 | } 55 | }; 56 | 57 | foreach ($graph->vertices() as $v) { 58 | if (!$indices->contains($v)) { 59 | $visit($v); 60 | } 61 | } 62 | 63 | return $visitor; 64 | } 65 | } -------------------------------------------------------------------------------- /src/Gliph/Exception/IncompatibleGraphTypeException.php: -------------------------------------------------------------------------------- 1 | vertices = new \SplObjectStorage(); 51 | $this->walking = new \SplObjectStorage(); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function ensureVertex($vertex) { 58 | if (!is_object($vertex)) { 59 | throw new InvalidVertexTypeException('Vertices must be objects; non-object provided.'); 60 | } 61 | 62 | if (!$this->hasVertex($vertex)) { 63 | $this->vertices[$vertex] = new \SplObjectStorage(); 64 | } 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | public function vertices() { 73 | $set = $this->getTraversableSplos($this->vertices); 74 | foreach ($set as $vertex) { 75 | yield $vertex; 76 | } 77 | $this->walking->detach($set); 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function hasVertex($vertex) { 84 | return $this->vertices->contains($vertex); 85 | } 86 | 87 | /** 88 | * {@inheritdoc} 89 | */ 90 | public function order() { 91 | return $this->vertices->count(); 92 | } 93 | 94 | /** 95 | * {@inheritdoc} 96 | */ 97 | public function size() { 98 | return $this->size; 99 | } 100 | 101 | /** 102 | * Helper function to ensure SPLOS traversal pointer is not overridden. 103 | * 104 | * This would otherwise occur if nested calls are made that traverse the 105 | * same SPLOS. This keeps track of which SPLOSes are currently being 106 | * traversed, and if it's in use, it returns a clone. 107 | * 108 | * It is incumbent on the calling code to release the semaphore directly 109 | * by calling $this->walking->detach($splos) when the traversal in 110 | * question is complete. (This is very important!) 111 | * 112 | * @param \SplObjectStorage $splos 113 | * The SPLOS to traverse. 114 | * 115 | * @return \SplObjectStorage 116 | * A SPLOS that is safe for traversal; may or may not be a clone of the 117 | * original. 118 | */ 119 | protected function getTraversableSplos(\SplObjectStorage $splos) { 120 | if ($this->walking->contains($splos)) { 121 | return clone $splos; 122 | } 123 | else { 124 | $this->walking->attach($splos); 125 | return $splos; 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /src/Gliph/Graph/Digraph.php: -------------------------------------------------------------------------------- 1 | B 20 | * 21 | * Note that, in set terms, 22 | * g.adjacentTo(x) == g.successorsOf(x) ∪ g.predecessorsOf(x) 23 | * 24 | * @param object $vertex 25 | * The vertex whose successor vertices should be enumerated. 26 | * 27 | * @return \Generator 28 | * A generator that yields successor vertices as values. 29 | * 30 | * @throws NonexistentBertexException 31 | * Thrown if the vertex provided is not present in the graph. 32 | */ 33 | public function successorsOf($vertex); 34 | 35 | /** 36 | * Enumerates each predecessor vertex to the provided vertex via a generator. 37 | * 38 | * A vertex (B) is a predecessor to another vertex (A) if an arc points from 39 | * B to A. In such an arc, B is considered the 'tail', A is considered the 40 | * 'head', as it would be visually represented with an arrow: 41 | * 42 | * B -> A 43 | * 44 | * Note that, in set terms, 45 | * g.adjacentTo(x) == g.successorsOf(x) ∪ g.predecessorsOf(x) 46 | * 47 | * @param object $vertex 48 | * The vertex whose predecessor vertices should be enumerated. 49 | * 50 | * @return \Generator 51 | * A generator that yields predecessor vertices as values. 52 | * 53 | * @throws NonexistentVertexException 54 | * Thrown if the vertex provided is not present in the graph. 55 | */ 56 | public function predecessorsOf($vertex); 57 | 58 | /** 59 | * Enumerates each out-arc from the provided vertex via a generator. 60 | * 61 | * An arc is outbound from a vertex if that vertex is the 'tail' of the arc. 62 | * 63 | * Returns a generator that yields 2-tuple (array) where the first two values 64 | * represent the vertex pair. Vertex order is guaranteed: the first vertex 65 | * is the tail, and the second vertex is the head. 66 | * 67 | * If the graph has additional edge data (e.g., weight), additional elements 68 | * are appended to the edge array as needed. (See implementation-specific 69 | * documentation for more detail). 70 | * 71 | * @see Digraph::arcsTo() 72 | * @see Digraph::successorsOf() 73 | * @see Graph::incidentTo() 74 | * 75 | * Note that, in set terms, 76 | * g.incidentTo(x) == g.arcsFrom(x) ∪ g.arcsTo(x) 77 | * 78 | * @param $vertex 79 | * The vertex whose out-arcs should be visited. 80 | * 81 | * @return \Generator 82 | * A generator that yields out-arcs as values. 83 | * 84 | * @throws NonexistentVertexException 85 | * Thrown if the vertex provided is not present in the graph. 86 | */ 87 | public function arcsFrom($vertex); 88 | 89 | /** 90 | * Enumerates each in-arc from the provided vertex via a generator. 91 | * 92 | * An arc is inbound to a vertex if that vertex is the 'head' of the arc. 93 | * 94 | * Returns a generator that yields 2-tuple (array) where the first two values 95 | * represent the vertex pair. Vertex order is guaranteed: the first vertex 96 | * is the tail, and the second vertex is the head. 97 | * 98 | * If the graph has additional edge data (e.g., weight), additional elements 99 | * are appended to the edge array as needed. (See implementation-specific 100 | * documentation for more detail). 101 | * 102 | * @see Digraph::arcsFrom() 103 | * @see Digraph::predecessorsOf() 104 | * @see Graph::incidentTo() 105 | * 106 | * Note that, in set terms, 107 | * g.incidentTo(x) == g.arcsFrom(x) ∪ g.arcsTo(x) 108 | * 109 | * @param $vertex 110 | * The vertex whose in-arcs should be visited. 111 | * 112 | * @return \Generator 113 | * A generator that yields in-arcs as values. 114 | * 115 | * @throws NonexistentVertexException 116 | * Thrown if the vertex provided is not present in the graph. 117 | */ 118 | public function arcsTo($vertex); 119 | 120 | /** 121 | * Returns the in-degree (number of incoming edges) for the provided vertex. 122 | * 123 | * In undirected graphs, in-degree and out-degree are the same. 124 | * 125 | * @param object $vertex 126 | * The vertex for which to retrieve in-degree information. 127 | * 128 | * @return int 129 | * 130 | * @throws NonexistentVertexException 131 | * Thrown if the vertex provided in the first parameter is not present in 132 | * the graph. 133 | * 134 | */ 135 | public function inDegreeOf($vertex); 136 | 137 | /** 138 | * Returns the out-degree (count of outgoing edges) for the provided vertex. 139 | * 140 | * In undirected graphs, in-degree and out-degree are the same. 141 | * 142 | * @param object $vertex 143 | * The vertex for which to retrieve out-degree information. 144 | * 145 | * @return int 146 | * 147 | * @throws NonexistentVertexException 148 | * Thrown if the vertex provided in the first parameter is not present in 149 | * the graph. 150 | * 151 | */ 152 | public function outDegreeOf($vertex); 153 | 154 | /** 155 | * Returns the transpose of this graph. 156 | * 157 | * A transpose is identical to the current graph, except that its edges 158 | * have had their directionality reversed. 159 | * 160 | * Transposed graphs are sometimes called the 'reverse' or 'converse'. 161 | * 162 | * @return Digraph 163 | */ 164 | public function transpose(); 165 | 166 | /** 167 | * Indicates whether or not this graph is acyclic. 168 | * 169 | * @return bool 170 | */ 171 | public function isAcyclic(); 172 | 173 | /** 174 | * Returns the cycles in this graph, if any. 175 | * 176 | * @return array 177 | * An array of arrays, each subarray representing a full cycle in the 178 | * graph. If the array is empty, the graph is acyclic. 179 | */ 180 | public function getCycles(); 181 | } -------------------------------------------------------------------------------- /src/Gliph/Graph/DirectedAdjacencyList.php: -------------------------------------------------------------------------------- 1 | hasVertex($vertex)) { 19 | throw new NonexistentVertexException('Vertex is not in graph; cannot iterate over its adjacent vertices.'); 20 | } 21 | 22 | $set = $this->getTraversableSplos($this->vertices[$vertex]); 23 | foreach ($set as $adjacent_vertex) { 24 | yield $adjacent_vertex; 25 | } 26 | $this->walking->detach($this->vertices); 27 | 28 | // Search inbound arcs 29 | $set = $this->getTraversableSplos($this->vertices); 30 | foreach ($set as $v) { 31 | if ($this->vertices[$v]->contains($vertex)) { 32 | yield $v; 33 | } 34 | } 35 | $this->walking->detach($set); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function successorsOf($vertex) { 42 | if (!$this->hasVertex($vertex)) { 43 | throw new NonexistentVertexException('Vertex is not in graph; cannot iterate over its successor vertices.'); 44 | } 45 | 46 | $set = $this->getTraversableSplos($this->vertices[$vertex]); 47 | foreach ($set as $successor) { 48 | yield $successor; 49 | } 50 | $this->walking->detach($set); 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function predecessorsOf($vertex) { 57 | if (!$this->hasVertex($vertex)) { 58 | throw new NonexistentVertexException('Vertex is not in graph; cannot iterate over its predecessor vertices.'); 59 | } 60 | 61 | $set = $this->getTraversableSplos($this->vertices); 62 | foreach ($set as $v) { 63 | if ($this->vertices[$v]->contains($vertex)) { 64 | yield $v; 65 | } 66 | } 67 | $this->walking->detach($set); 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function incidentTo($vertex) { 74 | if (!$this->hasVertex($vertex)) { 75 | throw new NonexistentVertexException('Vertex is not in graph; cannot iterate over its incident edges.'); 76 | } 77 | 78 | $set = $this->getTraversableSplos($this->vertices[$vertex]); 79 | foreach ($set as $adjacent_vertex) { 80 | yield array($vertex, $adjacent_vertex); 81 | } 82 | $this->walking->detach($set); 83 | 84 | $set = $this->getTraversableSplos($this->vertices); 85 | foreach ($set as $v) { 86 | if ($this->vertices[$v]->contains($vertex)) { 87 | yield array($v, $vertex); 88 | } 89 | } 90 | $this->walking->detach($set); 91 | } 92 | 93 | /** 94 | * {@inheritdoc} 95 | */ 96 | public function arcsFrom($vertex) { 97 | if (!$this->hasVertex($vertex)) { 98 | throw new NonexistentVertexException('Vertex is not in graph; cannot iterate over its successor vertices.'); 99 | } 100 | 101 | $set = $this->getTraversableSplos($this->vertices[$vertex]); 102 | foreach ($set as $successor) { 103 | yield array($vertex, $successor); 104 | } 105 | $this->walking->detach($set); 106 | } 107 | 108 | /** 109 | * {@inheritdoc} 110 | */ 111 | public function arcsTo($vertex) { 112 | if (!$this->hasVertex($vertex)) { 113 | throw new NonexistentVertexException('Vertex is not in graph; cannot iterate over its predecessor vertices.'); 114 | } 115 | 116 | $set = $this->getTraversableSplos($this->vertices); 117 | foreach ($set as $v) { 118 | if ($this->vertices[$v]->contains($vertex)) { 119 | yield array($v, $vertex); 120 | } 121 | } 122 | $this->walking->detach($set); 123 | } 124 | 125 | /** 126 | * {@inheritdoc} 127 | */ 128 | public function ensureArc($tail, $head) { 129 | $this->ensureVertex($tail)->ensureVertex($head); 130 | if (!$this->vertices[$tail]->contains($head)) { 131 | $this->size++; 132 | } 133 | 134 | $this->vertices[$tail]->attach($head); 135 | } 136 | 137 | /** 138 | * {@inheritdoc} 139 | */ 140 | public function removeVertex($vertex) { 141 | if (!$this->hasVertex($vertex)) { 142 | throw new NonexistentVertexException('Vertex is not in the graph, it cannot be removed.', E_WARNING); 143 | } 144 | 145 | foreach ($this->vertices() as $v) { 146 | $this->vertices[$v]->detach($vertex); 147 | } 148 | unset($this->vertices[$vertex]); 149 | } 150 | 151 | /** 152 | * {@inheritdoc} 153 | */ 154 | public function removeArc($tail, $head) { 155 | $this->vertices[$tail]->detach($head); 156 | } 157 | 158 | /** 159 | * {@inheritdoc} 160 | */ 161 | public function edges() { 162 | $oset = $this->getTraversableSplos($this->vertices); 163 | foreach ($oset as $tail) { 164 | $set = $this->getTraversableSplos($this->vertices[$tail]); 165 | foreach ($set as $head) { 166 | yield array($tail, $head); 167 | } 168 | $this->walking->detach($set); 169 | } 170 | $this->walking->detach($oset); 171 | } 172 | 173 | /** 174 | * {@inheritdoc} 175 | */ 176 | public function transpose() { 177 | $graph = new self(); 178 | foreach ($this->edges() as $edge) { 179 | $graph->ensureArc($edge[1], $edge[0]); 180 | } 181 | 182 | return $graph; 183 | } 184 | 185 | /** 186 | * {@inheritdoc} 187 | */ 188 | public function isAcyclic() { 189 | // The DepthFirstToposortVisitor throws an exception on cycles. 190 | try { 191 | DepthFirst::traverse($this, new DepthFirstToposortVisitor()); 192 | return TRUE; 193 | } 194 | catch (RuntimeException $e) { 195 | return FALSE; 196 | } 197 | } 198 | 199 | /** 200 | * {@inheritdoc} 201 | */ 202 | public function getCycles() { 203 | $scc = ConnectedComponent::tarjan_scc($this); 204 | return $scc->getConnectedComponents(); 205 | } 206 | 207 | /** 208 | * {@inheritdoc} 209 | */ 210 | public function inDegreeOf($vertex) { 211 | if (!$this->hasVertex($vertex)) { 212 | throw new NonexistentVertexException('Vertex is not in the graph, in-degree information cannot be provided', E_WARNING); 213 | } 214 | 215 | $count = 0; 216 | foreach ($this->vertices() as $v) { 217 | if ($this->vertices[$v]->contains($vertex)) { 218 | $count++; 219 | } 220 | } 221 | 222 | return $count; 223 | } 224 | 225 | /** 226 | * {@inheritdoc} 227 | */ 228 | public function outDegreeOf($vertex) { 229 | if (!$this->hasVertex($vertex)) { 230 | throw new NonexistentVertexException('Vertex is not in the graph, out-degree information cannot be provided', E_WARNING); 231 | } 232 | 233 | return $this->vertices[$vertex]->count(); 234 | } 235 | 236 | /** 237 | * {@inheritdoc} 238 | */ 239 | public function degreeOf($vertex) { 240 | return $this->inDegreeOf($vertex) + $this->outDegreeOf($vertex); 241 | } 242 | } 243 | 244 | -------------------------------------------------------------------------------- /src/Gliph/Graph/Graph.php: -------------------------------------------------------------------------------- 1 | hasVertex($vertex)) { 15 | throw new NonexistentVertexException('Vertex is not in graph; cannot iterate over its adjacent vertices.'); 16 | } 17 | 18 | $set = $this->getTraversableSplos($this->vertices[$vertex]); 19 | foreach ($set as $adjacent_vertex) { 20 | yield $adjacent_vertex; 21 | } 22 | $this->walking->detach($set); 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function incidentTo($vertex) { 29 | if (!$this->hasVertex($vertex)) { 30 | throw new NonexistentVertexException('Vertex is not in graph; cannot iterate over its incident edges.'); 31 | } 32 | 33 | $set = $this->getTraversableSplos($this->vertices[$vertex]); 34 | foreach ($set as $adjacent_vertex) { 35 | yield array($vertex, $adjacent_vertex); 36 | } 37 | $this->walking->detach($set); 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function ensureEdge($from, $to) { 44 | $this->ensureVertex($from)->ensureVertex($to); 45 | if (!$this->vertices[$from]->contains($to)) { 46 | $this->size++; 47 | } 48 | 49 | $this->vertices[$from]->attach($to); 50 | $this->vertices[$to]->attach($from); 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function removeVertex($vertex) { 57 | if (!$this->hasVertex($vertex)) { 58 | throw new NonexistentVertexException('Vertex is not in the graph, it cannot be removed.', E_WARNING); 59 | } 60 | 61 | foreach ($this->vertices[$vertex] as $adjacent) { 62 | $this->vertices[$adjacent]->detach($vertex); 63 | } 64 | unset($this->vertices[$vertex]); 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public function removeEdge($from, $to) { 71 | $this->vertices[$from]->detach($to); 72 | $this->vertices[$to]->detach($from); 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | */ 78 | public function edges() { 79 | $complete = new \SplObjectStorage(); 80 | 81 | $oset = $this->getTraversableSplos($this->vertices); 82 | foreach ($oset as $v) { 83 | $set = $this->getTraversableSplos($this->vertices[$v]); 84 | foreach ($set as $a) { 85 | if (!$complete->contains($a)) { 86 | yield array($v, $a); 87 | } 88 | } 89 | $complete->attach($v); 90 | $this->walking->detach($set); 91 | } 92 | $this->walking->detach($oset); 93 | } 94 | 95 | /** 96 | * {@inheritdoc} 97 | */ 98 | public function degreeOf($vertex) { 99 | if (!$this->hasVertex($vertex)) { 100 | throw new NonexistentVertexException('Vertex is not in the graph, in-degree information cannot be provided', E_WARNING); 101 | } 102 | 103 | return $this->vertices[$vertex]->count(); 104 | } 105 | } -------------------------------------------------------------------------------- /src/Gliph/Traversal/DepthFirst.php: -------------------------------------------------------------------------------- 1 | push($start); 46 | } 47 | 48 | if ($queue->isEmpty()) { 49 | throw new RuntimeException('No start vertex or vertices were provided, and no source vertices could be found in the provided graph.', E_WARNING); 50 | } 51 | 52 | $visiting = new \SplObjectStorage(); 53 | $visited = new \SplObjectStorage(); 54 | 55 | $visitor->beginTraversal(); 56 | 57 | $visit = function($vertex) use ($graph, $visitor, &$visit, $visiting, $visited) { 58 | if ($visiting->contains($vertex)) { 59 | $visitor->onBackEdge($vertex, $visit); 60 | } 61 | else if (!$visited->contains($vertex)) { 62 | $visiting->attach($vertex); 63 | 64 | $visitor->onStartVertex($vertex, $visit); 65 | 66 | foreach ($graph->successorsOf($vertex) as $head) { 67 | $visitor->onExamineEdge($vertex, $head, $visit); 68 | $visit($head); 69 | } 70 | 71 | $visitor->onFinishVertex($vertex, $visit); 72 | 73 | $visiting->detach($vertex); 74 | $visited->attach($vertex); 75 | } 76 | }; 77 | 78 | // TODO experiment with adding a generator-producing visitor method that yields the queue here 79 | while (!$queue->isEmpty()) { 80 | $vertex = $queue->shift(); 81 | $visit($vertex); 82 | } 83 | 84 | $visitor->endTraversal(); 85 | } 86 | 87 | /** 88 | * Finds source vertices in a Digraph, then enqueues them. 89 | * 90 | * @param Digraph $graph 91 | * @param DepthFirstVisitorInterface $visitor 92 | * 93 | * @return \SplQueue 94 | */ 95 | public static function find_sources(Digraph $graph, DepthFirstVisitorInterface $visitor) { 96 | $incomings = new \SplObjectStorage(); 97 | $queue = new \SplQueue(); 98 | 99 | foreach ($graph->edges() as $edge) { 100 | if (!isset($incomings[$edge[1]])) { 101 | $incomings[$edge[1]] = new \SplObjectStorage(); 102 | } 103 | $incomings[$edge[1]]->attach($edge[0]); 104 | } 105 | 106 | // Prime the queue with vertices that have no incoming edges. 107 | foreach ($graph->vertices() as $vertex) { 108 | if (!$incomings->contains($vertex)) { 109 | $queue->push($vertex); 110 | $visitor->onInitializeVertex($vertex, TRUE, $queue); 111 | } 112 | else { 113 | $visitor->onInitializeVertex($vertex, FALSE, $queue); 114 | } 115 | } 116 | 117 | return $queue; 118 | } 119 | 120 | /** 121 | * Performs a topological sort on the provided graph. 122 | * 123 | * @param Digraph $graph 124 | * @param object|\SplDoublyLinkedList $start 125 | * The starting point(s) for the toposort. @see DepthFirst::traverse() 126 | * 127 | * @return array 128 | * A valid topologically sorted list for the provided graph. 129 | */ 130 | public static function toposort(Digraph $graph, $start = NULL) { 131 | $visitor = new DepthFirstToposortVisitor(); 132 | self::traverse($graph, $visitor, $start); 133 | 134 | return $visitor->getTsl(); 135 | } 136 | } -------------------------------------------------------------------------------- /src/Gliph/Visitor/DepthFirstBasicVisitor.php: -------------------------------------------------------------------------------- 1 | active = new \SplObjectStorage(); 27 | $this->paths = new \SplObjectStorage(); 28 | } 29 | 30 | public function onInitializeVertex($vertex, $source, \SplQueue $queue) { 31 | parent::onInitializeVertex($vertex, $source, $queue); 32 | 33 | $this->paths[$vertex] = array(); 34 | } 35 | 36 | public function onStartVertex($vertex, \Closure $visit) { 37 | parent::onStartVertex($vertex, $visit); 38 | 39 | $this->active->attach($vertex); 40 | if (!isset($this->paths[$vertex])) { 41 | $this->paths[$vertex] = array(); 42 | } 43 | } 44 | 45 | public function onExamineEdge($from, $to, \Closure $visit) { 46 | parent::onExamineEdge($from, $to, $visit); 47 | 48 | foreach ($this->active as $vertex) { 49 | // TODO this check makes this less efficient - find a better algo 50 | if (!in_array($to, $this->paths[$vertex], TRUE)) { 51 | $path = $this->paths[$vertex]; 52 | $path[] = $to; 53 | $this->paths[$vertex] = $path; 54 | } 55 | } 56 | } 57 | 58 | public function onFinishVertex($vertex, \Closure $visit) { 59 | parent::onFinishVertex($vertex, $visit); 60 | 61 | $this->active->detach($vertex); 62 | } 63 | 64 | /** 65 | * Returns an array of all vertices reachable from the given vertex. 66 | * 67 | * @param object $vertex 68 | * The vertex for which reachability data is desired. 69 | * 70 | * @return array|bool 71 | * An array of reachable vertices, or FALSE if the vertex could not be 72 | * found in the reachability data. Note that an empty array will be 73 | * returned for vertices that zero reachable vertices. This is a different 74 | * from FALSE, so the identity operator (===) should be used to verify 75 | * returns. 76 | * 77 | * @throws WrongVisitorStateException 78 | * Thrown if reachability data is requested before the traversal algorithm 79 | * completes. 80 | */ 81 | public function getReachable($vertex) { 82 | if ($this->getState() !== self::COMPLETE) { 83 | throw new WrongVisitorStateException('Correct reachability data cannot be retrieved until traversal is complete.'); 84 | } 85 | 86 | if (!isset($this->paths[$vertex])) { 87 | return FALSE; 88 | } 89 | 90 | return $this->paths[$vertex]; 91 | } 92 | } -------------------------------------------------------------------------------- /src/Gliph/Visitor/DepthFirstNoOpVisitor.php: -------------------------------------------------------------------------------- 1 | tsl = array(); 45 | } 46 | 47 | public function onFinishVertex($vertex, \Closure $visit) { 48 | $this->tsl[] = $vertex; 49 | } 50 | 51 | /** 52 | * Returns a valid topological sort of the visited graph as an array. 53 | * 54 | * @return array 55 | * 56 | * @throws WrongVisitorStateException 57 | * Thrown if called before traversal is complete. 58 | */ 59 | public function getTsl() { 60 | if ($this->getState() !== self::COMPLETE) { 61 | throw new WrongVisitorStateException('Topologically sorted list cannot be retrieved until traversal is complete.'); 62 | } 63 | 64 | return $this->tsl; 65 | } 66 | } -------------------------------------------------------------------------------- /src/Gliph/Visitor/DepthFirstVisitorInterface.php: -------------------------------------------------------------------------------- 1 | getState() != self::IN_PROGRESS) { 40 | throw new WrongVisitorStateException('Cannot end traversal, visitor is not marked as currently being in progress.'); 41 | } 42 | $this->state = self::COMPLETE; 43 | } 44 | 45 | public function getState() { 46 | return $this->state; 47 | } 48 | } -------------------------------------------------------------------------------- /src/Gliph/Visitor/StatefulDepthFirstVisitor.php: -------------------------------------------------------------------------------- 1 | state != self::NOT_STARTED) { 30 | throw new WrongVisitorStateException('Vertex initialization should only happen before traversal has begun.'); 31 | } 32 | } 33 | 34 | public function beginTraversal() { 35 | if ($this->state != self::NOT_STARTED) { 36 | throw new WrongVisitorStateException('Traversal has already begun; cannot begin twice.'); 37 | } 38 | $this->state = self::IN_PROGRESS; 39 | } 40 | 41 | public function onBackEdge($vertex, \Closure $visit) { 42 | if ($this->state != self::IN_PROGRESS) { 43 | throw new WrongVisitorStateException('onBackEdge should only be called while traversal is in progress.'); 44 | } 45 | } 46 | 47 | public function onStartVertex($vertex, \Closure $visit) { 48 | if ($this->state != self::IN_PROGRESS) { 49 | throw new WrongVisitorStateException('onStartVertex should only be called while traversal is in progress.'); 50 | } 51 | } 52 | 53 | public function onExamineEdge($from, $to, \Closure $visit) { 54 | if ($this->state != self::IN_PROGRESS) { 55 | throw new WrongVisitorStateException('onExamineEdge should only be called while traversal is in progress.'); 56 | } 57 | } 58 | 59 | public function onFinishVertex($vertex, \Closure $visit) { 60 | if ($this->state != self::IN_PROGRESS) { 61 | throw new WrongVisitorStateException('onFinishVertex should only be called while traversal is in progress.'); 62 | } 63 | } 64 | 65 | public function endTraversal() { 66 | if ($this->state != self::IN_PROGRESS) { 67 | throw new WrongVisitorStateException('Cannot end traversal; no traversal is currently in progress.'); 68 | } 69 | $this->state = self::COMPLETE; 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function getState() { 76 | return $this->state; 77 | } 78 | } -------------------------------------------------------------------------------- /src/Gliph/Visitor/StatefulVisitorInterface.php: -------------------------------------------------------------------------------- 1 | currentComponent); 18 | $this->currentComponent = array(); 19 | $this->components[] = &$this->currentComponent; 20 | } 21 | 22 | public function addToCurrentComponent($vertex) { 23 | $this->currentComponent[] = $vertex; 24 | } 25 | 26 | public function getComponents() { 27 | return $this->components; 28 | } 29 | 30 | public function getConnectedComponents() { 31 | // TODO make this less stupid 32 | return array_values(array_filter($this->components, function($component) { 33 | return count($component) > 1; 34 | })); 35 | } 36 | } --------------------------------------------------------------------------------