├── .circleci └── config.yml ├── .gitignore ├── .gitmodules ├── .phpcs.xml ├── LICENSE ├── README.md ├── app ├── controllers │ └── Welcome.php ├── libraries │ └── StubLibrary.php ├── models │ └── StubModel.php └── views │ └── welcome_view.php ├── config ├── database.php ├── general.php ├── routes.php └── web_server │ ├── 20-example-graphp.conf │ └── 20-mike-graphp.conf ├── graphp ├── core │ ├── GPAsyncControllerHandler.php │ ├── GPConfig.php │ ├── GPController.php │ ├── GPControllerHandler.php │ ├── GPDatabase.php │ ├── GPEnv.php │ ├── GPErrorText.php │ ├── GPFileMap.php │ ├── GPLibrary.php │ ├── GPLoader.php │ ├── GPObject.php │ ├── GPRedirectControllerHandler.php │ ├── GPRequestData.php │ ├── GPRouter.php │ ├── GPSecurity.php │ ├── GPSession.php │ ├── GPTest.php │ ├── GPURIControllerHandler.php │ └── GPURLControllerHandler.php ├── db │ └── mysql_schema.sql ├── lib │ ├── GPProfiler.php │ └── GPRouteGenerator.php ├── model │ ├── GPBatch.php │ ├── GPBatchLoader.php │ ├── GPDataType.php │ ├── GPEdgeType.php │ ├── GPNode.php │ ├── GPNodeLoader.php │ ├── GPNodeMagicMethods.php │ ├── GPNodeMap.php │ └── traits │ │ ├── GPDataTypeCreator.php │ │ └── GPNodeEdgeCreator.php ├── tests │ ├── GPBatchLoaderTest.php │ ├── GPBatchTest.php │ ├── GPControllerHandlerTest.php │ ├── GPEdgeCountTest.php │ ├── GPEdgeInverseTest.php │ ├── GPEdgeTest.php │ ├── GPLoadByRangeTest.php │ ├── GPModelTest.php │ ├── GPTestLimitLoadTest.php │ ├── bootstrap.php │ └── run_tests.sh └── utils │ ├── Assert.php │ ├── STRUtils.php │ └── arrays.php ├── public └── index.php ├── sample_app ├── controllers │ ├── Posts.php │ ├── Users.php │ ├── Welcome.php │ └── admin │ │ ├── Admin.php │ │ └── AdminAjax.php ├── libraries │ └── StringLibrary.php ├── models │ ├── Comment.php │ ├── Post.php │ └── User.php └── views │ ├── admin │ ├── edge_view.php │ ├── explore_view.php │ ├── node_type_view.php │ └── node_view.php │ ├── layout │ └── admin_layout.php │ ├── login_view.php │ ├── one_post.php │ └── post_list.php └── third_party ├── composer.json ├── composer.lock └── composer.phar /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/php:7.2 6 | steps: 7 | - checkout 8 | test: 9 | docker: 10 | - image: circleci/php:7.2 11 | - image: circleci/mysql:5.7 12 | environment: 13 | MYSQL_ALLOW_EMPTY_PASSWORD: true 14 | MYSQL_ROOT_PASSWORD: 15 | MYSQL_DATABASE: graphp 16 | MYSQL_USER: graphp 17 | MYSQL_PASSWORD: graphp 18 | steps: 19 | - checkout 20 | - run: 21 | name: "Pull Submodules" 22 | command: | 23 | git submodule init 24 | git submodule update --remote 25 | - run: 26 | name: Waiting for MySQL to be ready 27 | command: | 28 | for i in `seq 1 30`; 29 | do 30 | nc -z 127.0.0.1 3306 && echo Success && exit 0 31 | echo -n . 32 | sleep 1 33 | done 34 | echo Failed waiting for MySQL && exit 1 35 | - run: 36 | name: "Create DB" 37 | command: | 38 | sudo apt-get install mysql-client 39 | mysql -h 127.0.0.1 -u graphp -pgraphp graphp < graphp/db/mysql_schema.sql 40 | sudo docker-php-ext-install mysqli 41 | - run: 42 | name: "Update composer" 43 | command: | 44 | cd third_party 45 | php composer.phar install 46 | - run: graphp/tests/run_tests.sh 47 | workflows: 48 | version: 2 49 | build_and_test: 50 | jobs: 51 | - build 52 | - test 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config/web_server/20-mike_graphp.config 2 | graphp/maps/ 3 | maps 4 | *.DS_Store 5 | graphp/tests/.phpunit.result.cache 6 | third_party/vendor/* 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libphutil"] 2 | path = third_party/libphutil 3 | url = git@github.com:facebook/libphutil.git -------------------------------------------------------------------------------- /.phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Codify PHP Coding Standards. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 0 13 | 14 | 15 | 0 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | /app/views/** 36 | 37 | 38 | /app/views/** 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | /app/views/** 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 109 | 110 | /app/views/** 111 | 112 | 113 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Michael Landau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CodeFactor](https://www.codefactor.io/repository/github/mikeland86/graphp/badge)](https://www.codefactor.io/repository/github/mikeland86/graphp) [![CircleCI](https://circleci.com/gh/mikeland86/graphp.svg?style=svg)](https://circleci.com/gh/mikeland86/graphp) 2 | 3 | graphp 4 | ====== 5 | 6 | The GraPHP web framework. 7 | 8 | The goal of this project is to build a lightweight web framework with a graph DB abstraction. It should be very easy to create the graph schema with no knowledge of of how the data is stored. Also, the schema should be incredibly flexible so you should never need migrations when adding new models (nodes), connections (edges), or data that lives in nodes. 9 | 10 | A couple of things that describe graphp: 11 | 12 | * Full MVC. Zero boilerplate controllers, models, and views. 13 | * Models are your schema. Defining data is up to you (but not required). 14 | * No migrations. Team members can add new models independently without conflicts 15 | * No DB queries, unless you want to. Transparent model makes it easy to see what happens under the hood. 16 | * DB API is designed for fast performance. No implicit joins or other magic, but expressive enough for nice readable code. 17 | * No CLI needed (but supported for cron and tests). 18 | * All classes are loaded on demand when used for the first time. 19 | * PHP 5.6+ 20 | 21 | A simple example: 22 | 23 | Define nodes (your model) with minimum boilerplate 24 | = 25 | 26 | ```php 27 | class User extends GPNode { 28 | protected static function getDataTypesImpl() { 29 | return [ 30 | GPDataType::string('name', $indexed = true), 31 | ]; 32 | } 33 | protected static function getEdgeTypesImpl() { 34 | return [ 35 | BankAccount::edge(), 36 | ]; 37 | } 38 | } 39 | ``` 40 | 41 | Define a model for bank account 42 | 43 | ```php 44 | // No need to declare data if you don't want to index it. 45 | class BankAccount extends GPNode {} 46 | ``` 47 | 48 | Create instances and edges between them: 49 | 50 | ```php 51 | $user = (new User())->setName('Jane Doe')->save(); 52 | $bank_account = (new BankAccount()) 53 | ->setData('accountNumber', 123) 54 | ->setData('balance', 125.05) 55 | ->save(); 56 | $user->addBankAccount($bank_account)->save(); 57 | ``` 58 | 59 | and load them later: 60 | 61 | ```php 62 | $user = User::getOneByName('Jane Doe'); 63 | $account = $user->loadBankAccount()->getOneBankAccount(); 64 | echo $account->getData('balance'); // 125.05 65 | ``` 66 | 67 | Controllers 68 | = 69 | ```php 70 | class MyController extends GPController { 71 | 72 | public function helloWorld() { 73 | GP::view('hello_world_view', ['title' => 'Hello World']); 74 | } 75 | 76 | public function doStuff() { 77 | // Do stuff and redirect 78 | OtherController::redirect()->method(); 79 | } 80 | } 81 | ``` 82 | 83 | Views 84 | = 85 | ```html 86 | 87 | 88 | <?= $title ?> 89 | </title? 90 | <body> 91 | <a href="<?= OtherController::URI()->someMethod() ?>">Go to other controller</a> 92 | </body> 93 | <html> 94 | ``` 95 | 96 | Libraries 97 | = 98 | Avoid bloating models and controllers with business logic. Instead, you can organize your logic in 99 | library classes that extend `GPLibrary`. These classes inherit all the abilities of Controllers so 100 | they can be called with `async()` or from the CLI, but they are not reachable on the browser. They 101 | also have access to any ControllerHandler you write. 102 | 103 | 104 | Set up instructions 105 | ====== 106 | * Install php-7.2+ mysql php-mysqli 107 | * Run `mysql -u db_user < graphp/db/mysql_schema.sql` to create the database. 108 | * Point your webserver to public directory. 109 | * Modify config files to suit your environment. 110 | * To check out sample app, change the general config 'app_folder' to "sample_app". 111 | 112 | 113 | FAQ 114 | ====== 115 | 116 | **What is a graph database and why should I use it?** 117 | A graph db is a database that uses graph structures for semantic queries with nodes, edges and properties to represent and store data (wikipedia). By giving our nodes, edges, and data nice human readable names we can write pretty, easy to understand code while storing the data in a way that is much more intuitive than relational dbs or key value stores. The flexible schema makes it easy to make structural changes to objects without having to write migrations or make any db changes. 118 | 119 | 120 | **What is a human readable graph? How does this lead to nicer code?** 121 | The following code loads friends and city for a user and all her friends: 122 | ```php 123 | $friends = $user->loadCity()->loadFriends()->getFriends(); 124 | batch($friends)->loadFriends()->loadCity(); 125 | ``` 126 | 127 | 128 | **What are magic methods, and what do you mean no boilerplate?** 129 | In graphp, node methods are defined by the graph structure. So if you create a user node with a friend edge and a city edge, you automatically can do things like: 130 | ```php 131 | $user->addFriend($friend)->save(); 132 | $city = $user->loadCity()->getCity(); 133 | $user->removeAllFriends()->save(); 134 | ``` 135 | There are no cli commands to create the node, there is no autogen code, and there is no copy paste boilerplace. All of these methods will work using the minimal node and edge information you provide. 136 | -------------------------------------------------------------------------------- /app/controllers/Welcome.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | class Welcome extends GPController { 4 | 5 | public function index() { 6 | GP::view('welcome_view'); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/libraries/StubLibrary.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | /** 4 | * This stub is here so that git keeps track of this folder. Feel free to delete 5 | */ 6 | final class StubLibrary extends GPObject { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /app/models/StubModel.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | /** 4 | * This stub is here so that git keeps track of this folder. Feel free to delete 5 | */ 6 | final class StubModel extends GPNode { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /app/views/welcome_view.php: -------------------------------------------------------------------------------- 1 | <html> 2 | <head> 3 | <title>Welcome page 4 | 16 | 17 | 18 |
19 |

Welcome to the GraPHP framework

20 |
21 | This is a work in progress, contribute on 22 | github 23 |
24 |
25 | 26 | -------------------------------------------------------------------------------- /config/database.php: -------------------------------------------------------------------------------- 1 | '127.0.0.1', 5 | 'database' => 'graphp', 6 | 'user' => 'graphp', 7 | 'pass' => 'graphp', 8 | 'port' => 0, 9 | ]; 10 | -------------------------------------------------------------------------------- /config/general.php: -------------------------------------------------------------------------------- 1 | true, 5 | 6 | 'domain' => 'localhost', 7 | 'use_index_php' => true, // To avoid this, you need to configure your server 8 | 'handler_suffix' => 'ControllerHandler', 9 | 10 | // Security 11 | 'salt' => 'CHANGE THIS TO ANY RANDOM STRING', 12 | 'admin_enabled' => true, 13 | 14 | 'cookie_exp' => '1209600', // Two weeks in seconds 15 | 'cookie_domain' => '', 16 | 'cookie_name' => 'session', 17 | 18 | 'view_404' => '', 19 | 'layout_404' => '', 20 | 21 | 'app_folder' => 'app', 22 | 23 | // This will automatically drop the DB before the first view is rendered 24 | 'disallow_view_db_access' => false, 25 | ]; 26 | -------------------------------------------------------------------------------- /config/routes.php: -------------------------------------------------------------------------------- 1 | ['welcome', 'index'], 6 | 7 | // Custom regex routes, "#" not allowed. Don't end with slash if 8 | // you want non slash to match (or use /?). Use capture groups for arguments: 9 | // '^/id/(\d+)/?$' => ['Controller', 'index'], passes the capture group match 10 | // into Controller::index method call. 11 | '^/user/([0-9]+)/?$' => ['welcome', 'index'], 12 | 13 | // For more complicated PHP based routing, extend GPRouteGenerator 14 | // '^/api/v1/(.*)$' => MyCustomRouteGenerator::class, 15 | ]; 16 | -------------------------------------------------------------------------------- /config/web_server/20-example-graphp.conf: -------------------------------------------------------------------------------- 1 | 2 | # Sample Debian lighttpd configuration file 3 | # 4 | 5 | $HTTP["host"] == "www.example.com" { 6 | server.document-root = "/path/to/graphp/public/" 7 | 8 | url.rewrite-once = ( 9 | "^(.*)$" => "/index.php/$1" 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /config/web_server/20-mike-graphp.conf: -------------------------------------------------------------------------------- 1 | 2 | # Sample Debian lighttpd configuration file 3 | # 4 | 5 | $HTTP["host"] == "localhost" { 6 | server.document-root = "/users/Mike/dev/graphp/public/" 7 | 8 | url.rewrite-once = ( 9 | "^/(js|css|fonts)(.*)$" => "$0", 10 | "^(.*)$" => "/index.php/$1" 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /graphp/core/GPAsyncControllerHandler.php: -------------------------------------------------------------------------------- 1 | handled = true; 9 | $uri = parent::handle($method, $args); 10 | $log = ini_get('error_log') ?: '/dev/null'; 11 | execx('php '.ROOT_PATH.'public/index.php %s >> '.$log.' 2>&1 &', $uri); 12 | } 13 | 14 | public function __destruct() { 15 | if (!$this->handled) { 16 | $this->handle('', []); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /graphp/core/GPConfig.php: -------------------------------------------------------------------------------- 1 | name = $name; 19 | $this->config = require_once ROOT_PATH.'config/'.$name.'.php'; 20 | } 21 | 22 | public function toArray() { 23 | return $this->config; 24 | } 25 | 26 | public function __get($name) { 27 | if (isset($this->config[$name])) { 28 | return $this->config[$name]; 29 | } 30 | throw new Exception($name.' is not in '.$this->name.' config', 1); 31 | } 32 | 33 | public static function from_file($file) { 34 | $path = ROOT_PATH.'config/'.$file; 35 | if (is_readable($path)) { 36 | return include_once($path); 37 | } 38 | return []; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /graphp/core/GPController.php: -------------------------------------------------------------------------------- 1 | true, 10 | 'redirect' => true, 11 | 'uri' => true, 12 | 'url' => true, 13 | ]; 14 | 15 | public function init() { 16 | $post_json_data = (array) json_decode(file_get_contents('php://input'), true); 17 | $this->post = new GPRequestData(array_merge_by_keys($_POST, $post_json_data)); 18 | $this->get = new GPRequestData($_GET); 19 | } 20 | 21 | public function __call($method_name, $args) { 22 | return self::handleStatic($method_name, $args); 23 | } 24 | 25 | public static function __callStatic($method_name, $args) { 26 | return self::handleStatic($method_name, $args); 27 | } 28 | 29 | public function __destruct() { 30 | GPDatabase::disposeAll(); 31 | } 32 | 33 | private static function handleStatic($method_name, $args) { 34 | $handler = $method_name.GPConfig::get()->handler_suffix; 35 | if ( 36 | !idx(self::$coreHandlers, strtolower($method_name)) && 37 | is_subclass_of($handler, GPControllerHandler::class) 38 | ) { 39 | return $handler::get(get_called_class()); 40 | } 41 | $core_handler = 'GP'.$method_name.GPConfig::get()->handler_suffix; 42 | if (is_subclass_of($core_handler, GPControllerHandler::class)) { 43 | return $core_handler::get(get_called_class()); 44 | } 45 | if (GPEnv::isDevEnv()) { 46 | echo 'Method "'.$method_name.'" is not in '.get_called_class(); 47 | } 48 | GP::return404(); 49 | } 50 | 51 | /** 52 | * Override this to limit access to individual methods or the entire controller 53 | * @param string 54 | * @return boolean 55 | */ 56 | public function isAllowed(string $method): bool { 57 | return true; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /graphp/core/GPControllerHandler.php: -------------------------------------------------------------------------------- 1 | Return if user has access to controller/method 11 | * -> Return API endpoint of method 12 | * -> Return endpoint as a manipulatable object 13 | * -> Return annotations for given controller method 14 | */ 15 | abstract class GPControllerHandler extends GPObject { 16 | 17 | protected $controller; 18 | 19 | public function __construct($controller) { 20 | $this->controller = $controller; 21 | } 22 | 23 | // Override to implement handler functionality 24 | abstract protected function handle($method, array $args); 25 | 26 | // Override for more complicated functionality/caching 27 | public static function get($controller) { 28 | return new static($controller); 29 | } 30 | 31 | public function __call($method, array $args) { 32 | $this->validateMethod($method); 33 | return $this->handle($method, $args); 34 | } 35 | 36 | protected function validateMethod($method) { 37 | if ($method) { 38 | if ( 39 | !method_exists($this->controller, $method) || 40 | !(new ReflectionMethod($this->controller, $method))->isPublic() 41 | ) { 42 | throw new GPException( 43 | $this->controller.' does not have a public method '.$method 44 | ); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /graphp/core/GPDatabase.php: -------------------------------------------------------------------------------- 1 | guard = new AphrontWriteGuard(function() { 27 | if (GP::isCLI() || self::$skipWriteGuard) { 28 | return; 29 | } 30 | if (idx($_SERVER, 'REQUEST_METHOD') !== 'POST') { 31 | throw new Exception( 32 | 'You can only write to the database on post requests. If you need to 33 | make writes on get request, 34 | use GPDatabase::get()->beginUnguardedWrites()', 35 | 1 36 | ); 37 | } 38 | if (!GPSecurity::isCSFRTokenValid(idx($_POST, 'csrf'))) { 39 | throw new Exception( 40 | 'The request did not have a valid csrf token. This may be an attack or 41 | you may have forgetten to include it in a post request that does 42 | writes', 43 | 1 44 | ); 45 | } 46 | }); 47 | $config = GPConfig::get($config_name); 48 | $this->connection = new AphrontMySQLiDatabaseConnection($config->toArray()); 49 | } 50 | 51 | public function getConnection() { 52 | Assert::equals( 53 | self::$viewLock, 54 | 0, 55 | 'Tried to access DB while view lock is on' 56 | ); 57 | Assert::truthy($this->connection, 'DB Connection no longer exists'); 58 | return $this->connection; 59 | } 60 | 61 | public function beginUnguardedWrites() { 62 | AphrontWriteGuard::beginUnguardedWrites(); 63 | } 64 | 65 | public function endUnguardedWrites() { 66 | AphrontWriteGuard::endUnguardedWrites(); 67 | } 68 | 69 | public function skipWriteGuard() { 70 | self::$skipWriteGuard = true; 71 | } 72 | 73 | public function startTransaction() { 74 | if ($this->nestedTransactions === 0) { 75 | queryfx($this->getConnection(), 'START TRANSACTION;'); 76 | } 77 | $this->nestedTransactions++; 78 | } 79 | 80 | public function commit() { 81 | $this->nestedTransactions--; 82 | if ($this->nestedTransactions === 0) { 83 | queryfx($this->getConnection(), 'COMMIT;'); 84 | } 85 | } 86 | 87 | public function insertNode(GPNode $node) { 88 | queryfx( 89 | $this->getConnection(), 90 | 'INSERT INTO node (type, data) VALUES (%d, %s)', 91 | $node::getType(), 92 | $node->getJSONData() 93 | ); 94 | return $this->getConnection()->getInsertID(); 95 | } 96 | 97 | public function updateNodeData(GPNode $node) { 98 | queryfx( 99 | $this->getConnection(), 100 | 'UPDATE node SET data = %s WHERE id = %d', 101 | $node->getJSONData(), 102 | $node->getID() 103 | ); 104 | } 105 | 106 | public function getNodeByID($id) { 107 | return queryfx_one( 108 | $this->getConnection(), 109 | self::NODE_SELECT.' FROM node WHERE id = %d;', 110 | $id 111 | ); 112 | } 113 | 114 | public function getNodeByIDType($id, $type) { 115 | return queryfx_one( 116 | $this->getConnection(), 117 | self::NODE_SELECT.' FROM node WHERE id = %d AND type = %d;', 118 | $id, 119 | $type 120 | ); 121 | } 122 | 123 | public function multigetNodeByIDType(array $ids, $type = null) { 124 | if (!$ids) { 125 | return []; 126 | } 127 | $type_fragment = $type ? 'AND type = %d;' : ';'; 128 | $args = array_filter([$ids, $type]); 129 | return queryfx_all( 130 | $this->getConnection(), 131 | self::NODE_SELECT.' FROM node WHERE id IN (%Ld) '.$type_fragment, 132 | ...$args 133 | ); 134 | } 135 | 136 | public function getNodeIDsByTypeDataAll(int $type): array { 137 | return ipull(queryfx_all( 138 | $this->getConnection(), 139 | 'SELECT node_id FROM node_data WHERE type = %d;', 140 | $type 141 | ), 'node_id'); 142 | } 143 | 144 | public function getNodeIDsByTypeData($type, array $data) { 145 | if (!$data) { 146 | return []; 147 | } 148 | return ipull(queryfx_all( 149 | $this->getConnection(), 150 | 'SELECT node_id FROM node_data WHERE type = %d AND data IN (%Ls);', 151 | $type, 152 | $data 153 | ), 'node_id'); 154 | } 155 | 156 | public function getNodeIDsByTypeDataRange( 157 | $type, 158 | $start, 159 | $end, 160 | $limit, 161 | $offset 162 | ) { 163 | return ipull(queryfx_all( 164 | $this->getConnection(), 165 | 'SELECT node_id FROM node_data WHERE '. 166 | 'type = %d AND data >= %s AND data <= %s ORDER BY updated ASC LIMIT %d, %d;', 167 | $type, 168 | $start, 169 | $end, 170 | $offset, 171 | $limit 172 | ), 'node_id'); 173 | } 174 | 175 | public function updateNodeIndexedData(GPNode $node) { 176 | $values = []; 177 | $parts = []; 178 | foreach ($node->getIndexedData() as $name => $val) { 179 | $parts[] = '(%d, %d, %s)'; 180 | $values[] = $node->getID(); 181 | $values[] = $node::getDataTypeByName($name)->getIndexedType(); 182 | $values[] = $val; 183 | } 184 | $this->startTransaction(); 185 | $result = queryfx( 186 | $this->getConnection(), 187 | 'DELETE FROM node_data WHERE node_id = %d', 188 | $node->getID() 189 | ); 190 | if ($parts) { 191 | queryfx( 192 | $this->getConnection(), 193 | 'INSERT INTO node_data (node_id, type, data) VALUES '. 194 | implode(',', $parts).' ON DUPLICATE KEY UPDATE data = VALUES(data);', 195 | ...$values 196 | ); 197 | } 198 | $this->commit(); 199 | } 200 | 201 | private function getEdgeParts(GPNode $from_node, array $array_of_arrays) { 202 | $values = []; 203 | $parts = []; 204 | foreach ($array_of_arrays as $edge_type => $to_nodes) { 205 | foreach ($to_nodes as $to_node) { 206 | $parts[] = '(%d, %d, %d)'; 207 | array_push($values, $from_node->getID(), $to_node->getID(), $edge_type); 208 | } 209 | } 210 | return [$parts, $values]; 211 | } 212 | 213 | public function saveEdges(GPNode $from_node, array $array_of_arrays) { 214 | list($parts, $values) = $this->getEdgeParts($from_node, $array_of_arrays); 215 | if (!$parts) { 216 | return; 217 | } 218 | queryfx( 219 | $this->getConnection(), 220 | 'INSERT IGNORE INTO edge (from_node_id, to_node_id, type) VALUES '. 221 | implode(',', $parts).';', 222 | ...$values 223 | ); 224 | } 225 | 226 | public function deleteEdges(GPNode $from_node, array $array_of_arrays) { 227 | list($parts, $values) = $this->getEdgeParts($from_node, $array_of_arrays); 228 | if (!$parts) { 229 | return; 230 | } 231 | queryfx( 232 | $this->getConnection(), 233 | 'DELETE FROM edge WHERE (from_node_id, to_node_id, type) IN ('. 234 | implode(',', $parts).');', 235 | ...$values 236 | ); 237 | } 238 | 239 | public function deleteAllEdges(GPNode $node, array $edge_types) { 240 | return $this->deleteAllEdgesInternal($node, $edge_types, 'from_node_id'); 241 | } 242 | 243 | public function deleteAllInverseEdges(GPNode $node, array $edge_types) { 244 | return $this->deleteAllEdgesInternal($node, $edge_types, 'to_node_id'); 245 | } 246 | 247 | private function deleteAllEdgesInternal( 248 | GPNode $node, 249 | array $edge_types, 250 | $col 251 | ) { 252 | $values = []; 253 | $parts = []; 254 | foreach ($edge_types as $edge_type) { 255 | $parts[] = '(%d, %d)'; 256 | array_push($values, $node->getID(), $edge_type); 257 | } 258 | if (!$parts) { 259 | return; 260 | } 261 | queryfx( 262 | $this->getConnection(), 263 | 'DELETE FROM edge WHERE ('.$col.', type) IN ('. 264 | implode(',', $parts).');', 265 | ...$values 266 | ); 267 | } 268 | 269 | public function getConnectedIDs( 270 | GPNode $from_node, 271 | array $types, 272 | $limit = null, 273 | $offset = 0 274 | ) { 275 | return idx( 276 | $this->multiGetConnectedIDs([$from_node], $types, $limit, $offset), 277 | $from_node->getID(), 278 | array_fill_keys($types, []) 279 | ); 280 | } 281 | 282 | public function multiGetConnectedIDs( 283 | array $from_nodes, 284 | array $types, 285 | $limit = null, 286 | $offset = 0 287 | ) { 288 | if (!$types || !$from_nodes) { 289 | return []; 290 | } 291 | $args = [mpull($from_nodes, 'getID'), $types]; 292 | if ($limit !== null) { 293 | $args[] = $offset; 294 | $args[] = $limit; 295 | } 296 | $results = queryfx_all( 297 | $this->getConnection(), 298 | 'SELECT from_node_id, to_node_id, type FROM edge '. 299 | 'WHERE from_node_id IN (%Ld) AND type IN (%Ld) ORDER BY updated DESC'. 300 | ($limit === null ? '' : ' LIMIT %d, %d').';', 301 | ...$args 302 | ); 303 | $ordered = []; 304 | foreach ($results as $result) { 305 | if (!array_key_exists($result['from_node_id'], $ordered)) { 306 | $ordered[$result['from_node_id']] = array_fill_keys($types, []); 307 | } 308 | $ordered[$result['from_node_id']][$result['type']][$result['to_node_id']] 309 | = $result['to_node_id']; 310 | } 311 | return $ordered; 312 | } 313 | 314 | public function getConnectedNodeCount(array $nodes, array $edges) { 315 | $results = queryfx_all( 316 | $this->getConnection(), 317 | 'SELECT from_node_id, type, count(1) as c FROM edge '. 318 | 'WHERE from_node_id IN (%Ld) AND type IN (%Ld) group by from_node_id, '. 319 | 'type;', 320 | mpull($nodes, 'getID'), 321 | mpull($edges, 'getType') 322 | ); 323 | return igroup($results, 'from_node_id'); 324 | } 325 | 326 | public function getAllByType($type, $limit, $offset) { 327 | return queryfx_all( 328 | $this->getConnection(), 329 | self::NODE_SELECT.' FROM node WHERE type = %d ORDER BY updated DESC LIMIT %d, %d;', 330 | $type, 331 | $offset, 332 | $limit 333 | ); 334 | } 335 | 336 | public function getTypeCounts() { 337 | return queryfx_all( 338 | $this->getConnection(), 339 | 'SELECT type, COUNT(1) AS count FROM node GROUP BY type;' 340 | ); 341 | } 342 | 343 | public function deleteNodes(array $nodes) { 344 | if (!$nodes) { 345 | return; 346 | } 347 | queryfx( 348 | $this->getConnection(), 349 | 'DELETE FROM node WHERE id IN (%Ld);', 350 | mpull($nodes, 'getID') 351 | ); 352 | } 353 | 354 | public static function incrementViewLock() { 355 | self::$viewLock++; 356 | } 357 | 358 | public static function decrementViewLock() { 359 | self::$viewLock--; 360 | } 361 | 362 | public function dispose() { 363 | if ($this->guard->isGuardActive()) { 364 | $this->guard->dispose(); 365 | } 366 | if ($this->connection) { 367 | $this->connection->close(); 368 | $this->connection = null; 369 | } 370 | } 371 | 372 | public static function disposeAll() { 373 | foreach (self::$dbs as $db) { 374 | $db->dispose(); 375 | } 376 | } 377 | 378 | public function __destruct() { 379 | $this->dispose(); 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /graphp/core/GPEnv.php: -------------------------------------------------------------------------------- 1 | is_dev; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /graphp/core/GPErrorText.php: -------------------------------------------------------------------------------- 1 | dirs = make_array($dirs); 11 | $this->name = $name; 12 | $map = is_readable($this->buildPath()) ? include $this->buildPath() : null; 13 | $this->map = $map ?: []; 14 | } 15 | 16 | public function getPath($file) { 17 | $file = strtolower($file); 18 | if (!isset($this->map[$file]) || !file_exists($this->map[$file])) { 19 | $this->regenMap(); 20 | } 21 | return idx($this->map, $file); 22 | } 23 | 24 | public function regenMap() { 25 | $this->map = []; 26 | foreach ($this->dirs as $dir) { 27 | $dir_iter = new RecursiveDirectoryIterator($dir); 28 | $iter = new RecursiveIteratorIterator($dir_iter); 29 | 30 | foreach ($iter as $key => $file) { 31 | if ($file->getExtension() === 'php') { 32 | list($name) = explode('.', $file->getFileName()); 33 | $this->map[strtolower($name)] = $key; 34 | } 35 | } 36 | } 37 | $this->writeMap(); 38 | } 39 | 40 | public function getAllFileNames() { 41 | $this->regenMap(); 42 | return array_keys($this->map); 43 | } 44 | 45 | private function writeMap() { 46 | $map_file = "map as $file => $path) { 48 | $map_file .= ' \''.$file.'\' => \''.$path."',\n"; 49 | } 50 | $map_file .= "];\n"; 51 | $file_path = $this->buildPath(); 52 | $does_file_exist = file_exists($file_path); 53 | file_put_contents($file_path, $map_file); 54 | if (!$does_file_exist) { 55 | // File was just created, make sure to make it readable 56 | chmod($file_path, 0666); 57 | } 58 | } 59 | 60 | private function buildPath() { 61 | return '/tmp/maps/'.GPConfig::get()->app_folder.'_'.$this->name; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /graphp/core/GPLibrary.php: -------------------------------------------------------------------------------- 1 | app_folder; 28 | self::$map = new GPFileMap( 29 | [ 30 | ROOT_PATH.$app_folder.'/models', 31 | ROOT_PATH.'graphp', 32 | ROOT_PATH.$app_folder.'/libraries', 33 | ROOT_PATH.$app_folder.'/controllers', 34 | ], 35 | 'file_map' 36 | ); 37 | } 38 | return self::$map; 39 | } 40 | 41 | private static function GPAutoloader($class_name) { 42 | $path = self::getMap()->getPath($class_name); 43 | if ($path) { 44 | require_once $path; 45 | } 46 | } 47 | 48 | public static function view($view_name, array $_data = [], $return = false) { 49 | if (GPConfig::get()->disallow_view_db_access) { 50 | GPDatabase::incrementViewLock(); 51 | } 52 | $file = 53 | ROOT_PATH.GPConfig::get()->app_folder.'/views/'.$view_name.'.php'; 54 | if (!file_exists($file)) { 55 | throw new GPException('View "'.$view_name.'"" not found'); 56 | } 57 | $new_data = array_diff_key($_data, self::$viewData); 58 | $replaced_data = array_intersect_key(self::$viewData, $_data); 59 | self::$viewData = array_merge_by_keys(self::$viewData, $_data); 60 | ob_start(); 61 | extract(self::$globalViewData); 62 | extract(self::$viewData); 63 | require $file; 64 | // Return $viewData to the previous state to avoid view data bleeding. 65 | self::$viewData = array_merge_by_keys( 66 | array_diff_key(self::$viewData, $new_data), 67 | $replaced_data 68 | ); 69 | if (GPConfig::get()->disallow_view_db_access) { 70 | GPDatabase::decrementViewLock(); 71 | } 72 | if ($return) { 73 | $buffer = ob_get_contents(); 74 | @ob_end_clean(); 75 | return $buffer; 76 | } 77 | ob_end_flush(); 78 | } 79 | 80 | public static function viewWithLayout( 81 | $view, 82 | $layout, 83 | array $data = [], 84 | array $layout_data = [] 85 | ) { 86 | if (array_key_exists('content', $layout_data)) { 87 | throw new GPException('Key: \'content\' cannot be int layout_data'); 88 | } 89 | $layout_data['content'] = GP::view($view, $data, true); 90 | GP::view($layout, $layout_data); 91 | } 92 | 93 | public static function isCLI() { 94 | return php_sapi_name() === 'cli'; 95 | } 96 | 97 | public static function isAjax() { 98 | return 99 | filter_input(INPUT_SERVER, 'HTTP_X_REQUESTED_WITH') === 'XMLHttpRequest'; 100 | } 101 | 102 | public static function isJSONRequest() { 103 | return idx($_SERVER, 'CONTENT_TYPE') === 'application/json'; 104 | } 105 | 106 | public static function return404() { 107 | GPDatabase::get()->dispose(); 108 | http_response_code(404); 109 | $config = GPConfig::get(); 110 | if (GP::isAjax() || GP::isJSONRequest()) { 111 | GP::ajax(['error_code' => 404, 'error' => 'Resource Not Found']); 112 | } else if ($config->redirect_404) { 113 | header('Location: '.$config->redirect_404, true); 114 | } else if ($config->view_404 && $config->layout_404) { 115 | GP::viewWithLayout($config->view_404, $config->layout_404); 116 | } else if ($config->view_404) { 117 | GP::view($config->view_404); 118 | } else { 119 | echo '404'; 120 | } 121 | die(); 122 | } 123 | 124 | public static function return403() { 125 | GPDatabase::get()->dispose(); 126 | http_response_code(403); 127 | if (GP::isAjax() || GP::isJSONRequest()) { 128 | GP::ajax(['error_code' => 403, 'error' => 'Forbidden']); 129 | } else { 130 | echo 'Forbidden'; 131 | } 132 | die(); 133 | } 134 | 135 | public static function ajax(array $data) { 136 | header('Expires: 0'); 137 | header( 138 | 'Cache-Control: no-cache, must-revalidate, post-check=0, pre-check=0' 139 | ); 140 | header('Pragma: no-cache'); 141 | header('Content-type: application/json'); 142 | echo json_encode($data); 143 | GP::exit(); 144 | } 145 | 146 | public static function exit() { 147 | GPDatabase::disposeALl(); 148 | exit(); 149 | } 150 | 151 | public static function addGlobal($key, $val) { 152 | self::$globalViewData[$key] = $val; 153 | } 154 | } 155 | 156 | class_alias('GPLoader', 'GP'); 157 | 158 | // To instanciate a new GPLoader we need to call this once. 159 | GP::init(); 160 | -------------------------------------------------------------------------------- /graphp/core/GPObject.php: -------------------------------------------------------------------------------- 1 | getConstants(); 29 | self::$classConstantsFlip[get_called_class()] = 30 | array_flip(self::$classConstants[get_called_class()]); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /graphp/core/GPRedirectControllerHandler.php: -------------------------------------------------------------------------------- 1 | handled = true; 9 | // The autoloader will not work if PHP is shutting down. 10 | if (class_exists('GPDatabase', false)) { 11 | GPDatabase::disposeAll(); 12 | } 13 | $uri = parent::handle($method, $args); 14 | header('Location: '.$uri, true, 307); 15 | die(); 16 | } 17 | 18 | public function disableDestructRedirect() { 19 | $this->handled = true; 20 | } 21 | 22 | /** 23 | * This will redirect when the handler is destroyed allowing for shorter code 24 | * like: 25 | * MyController::redirect(); 26 | * instead of 27 | * MyController::redirect()->index(); 28 | */ 29 | public function __destruct() { 30 | if (!$this->handled) { 31 | $this->handle('', []); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /graphp/core/GPRequestData.php: -------------------------------------------------------------------------------- 1 | data = $data; 9 | } 10 | 11 | public function getData() { 12 | return $this->data; 13 | } 14 | 15 | public function getInt($key, $default = null) { 16 | $val = $this->get($key, 'is_numeric'); 17 | return $val !== null ? (int) $val : $default; 18 | } 19 | 20 | public function getString($key, $default = null) { 21 | $val = $this->get($key, 'is_string'); 22 | return $val !== null ? $val : $default; 23 | } 24 | 25 | public function getNumeric($key, $default = null) { 26 | $val = $this->get($key, 'is_numeric'); 27 | return $val !== null ? $val : $default; 28 | } 29 | 30 | public function getArray($key, $default = null) { 31 | $val = $this->get($key, 'is_array'); 32 | return $val !== null ? $val : $default; 33 | } 34 | 35 | public function getExists($key) { 36 | return array_key_exists($key, $this->data); 37 | } 38 | 39 | public function get($key, callable $validator = null) { 40 | $value = idx($this->data, $key); 41 | if ($validator === null || $validator($value)) { 42 | return $value; 43 | } 44 | return null; 45 | } 46 | 47 | public function serialize() { 48 | return json_encode($this->data); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /graphp/core/GPRouter.php: -------------------------------------------------------------------------------- 1 | getController(); 25 | 26 | if (!class_exists($controller_name)) { 27 | GP::return404(); 28 | } 29 | 30 | self::$controller = new $controller_name(); 31 | self::$method = $generator->getMethod(); 32 | 33 | if (!method_exists(self::$controller, self::$method)) { 34 | GP::return404(); 35 | } 36 | 37 | if (!self::$controller->isAllowed(self::$method)) { 38 | GP::return404(); 39 | } 40 | 41 | self::$controller->init(); 42 | call_user_func_array([self::$controller, self::$method], $generator->getArgs()); 43 | } 44 | 45 | public static function getController() { 46 | return self::$controller; 47 | } 48 | 49 | public static function getMethod() { 50 | return self::$method; 51 | } 52 | 53 | private static function getParts() { 54 | if (GP::isCLI()) { 55 | global $argv; 56 | $uri = idx($argv, 1, ''); 57 | } else { 58 | $uri = $_SERVER['REQUEST_URI']; 59 | } 60 | $uri = str_replace('index.php', '', $uri); 61 | $uri = preg_replace(['/\?.*/', '#[/]+$#'], '', $uri); 62 | if (!$uri && isset(self::$routes['__default__'])) { 63 | return self::$routes['__default__']; 64 | } 65 | foreach (array_keys(self::$routes) as $regex) { 66 | $matches = []; 67 | if (preg_match('#'.$regex.'#', $uri, $matches)) { 68 | $parts = self::$routes[$regex]; 69 | if (is_array($parts)) { 70 | array_concat_in_place($parts, array_slice($matches, 1)); 71 | } 72 | return $parts; 73 | } 74 | } 75 | return array_values(array_filter( 76 | explode('/', $uri), 77 | function($part) { 78 | return mb_strlen($part) > 0; // Stupid PHP filters out '0' 79 | } 80 | )); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /graphp/core/GPSecurity.php: -------------------------------------------------------------------------------- 1 | salt.$id); 18 | } 19 | 20 | public static function csrf() { 21 | if (self::$token === null) { 22 | self::$token = self::getNewCSRFToken(); 23 | } 24 | return ''; 25 | } 26 | 27 | public static function isCSFRTokenValid($token) { 28 | $timestamp = mb_substr($token, 64, null, '8bit'); 29 | if ($timestamp < time() - self::EXPIRATION) { 30 | return false; 31 | } 32 | $id = GPSession::get(GPSession::UID); 33 | return self::hmacVerify($token, self::$config->salt.$id); 34 | } 35 | 36 | public static function hmacSign($message, $key) { 37 | return hash_hmac('sha256', $message, $key).$message; 38 | } 39 | 40 | public static function hmacVerify($bundle, $key) { 41 | $msgMAC = mb_substr($bundle, 0, 64, '8bit'); 42 | $message = self::hmacGetMessage($bundle); 43 | // For PHP 5.5 compat 44 | if (function_exists('hash_equals')) { 45 | return hash_equals( 46 | hash_hmac('sha256', $message, $key), 47 | $msgMAC 48 | ); 49 | } 50 | return hash_hmac('sha256', $message, $key) === $msgMAC; 51 | } 52 | 53 | public static function hmacGetMessage($bundle) { 54 | return mb_substr($bundle, 64, null, '8bit'); 55 | } 56 | } 57 | 58 | GPSecurity::init(); 59 | -------------------------------------------------------------------------------- /graphp/core/GPSession.php: -------------------------------------------------------------------------------- 1 | cookie_name, ''); 13 | $json = '[]'; 14 | if ($json_with_hash) { 15 | if (!GPSecurity::hmacVerify($json_with_hash, self::$config->salt)) { 16 | self::destroy(); 17 | throw new Exception('Cookie hash missmatch. Possible attack', 1); 18 | } 19 | $json = mb_substr($json_with_hash, 64, null, '8bit'); 20 | } 21 | self::$session = json_decode($json, true); 22 | if (!self::get(self::UID)) { 23 | self::set(self::UID, base64_encode(microtime().mt_rand())); 24 | } 25 | } 26 | 27 | public static function get($key, $default = null) { 28 | return idx(self::$session, $key, $default); 29 | } 30 | 31 | public static function set($key, $val) { 32 | self::$session[$key] = $val; 33 | self::updateCookie(); 34 | } 35 | 36 | public static function delete($key) { 37 | unset(self::$session[$key]); 38 | self::updateCookie(); 39 | } 40 | 41 | public static function destroy() { 42 | if (GP::isCLI()) { 43 | return; 44 | } 45 | setcookie( 46 | self::$config->cookie_name, 47 | '', 48 | 0, 49 | '/', 50 | self::$config->cookie_domain 51 | ); 52 | } 53 | 54 | private static function updateCookie() { 55 | if (GP::isCLI()) { 56 | return; 57 | } 58 | $json = json_encode(self::$session); 59 | $json_with_hash = GPSecurity::hmacSign($json, self::$config->salt); 60 | if (strlen($json_with_hash) > 4093) { 61 | throw new Exception( 62 | 'Your session cookie is too large. That may break in some browsers. 63 | Consider storing large info in the DB.', 64 | 1 65 | ); 66 | } 67 | $result = setcookie( 68 | self::$config->cookie_name, 69 | $json_with_hash, 70 | time() + self::$config->cookie_exp, 71 | '/', 72 | self::$config->cookie_domain 73 | ); 74 | if (!$result) { 75 | throw new Exception( 76 | 'Error setting session cookie, make sure not to print any content 77 | prior to setting cookie', 78 | 1 79 | ); 80 | 81 | } 82 | } 83 | } 84 | 85 | GPSession::init(); 86 | -------------------------------------------------------------------------------- /graphp/core/GPTest.php: -------------------------------------------------------------------------------- 1 | use_index_php ? '/index.php' : ''; 9 | return $index.'/'.strtolower($this->controller).'/'.$method. 10 | ($args ? '/'.implode('/', $args) : ''); 11 | } 12 | 13 | public function __toString() { 14 | return $this->handle('', []); 15 | } 16 | 17 | public function jsonSerialize() { 18 | return (string) $this; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /graphp/core/GPURLControllerHandler.php: -------------------------------------------------------------------------------- 1 | domain.parent::handle($method, $args); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /graphp/db/mysql_schema.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE DATABASE IF NOT EXISTS graphp; 3 | 4 | USE graphp; 5 | 6 | SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO"; 7 | SET time_zone = "+00:00"; 8 | 9 | 10 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 11 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 12 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 13 | /*!40101 SET NAMES utf8 */; 14 | 15 | -- 16 | -- Database: `graphp` 17 | -- 18 | 19 | -- -------------------------------------------------------- 20 | 21 | -- 22 | -- Table structure for table `edge` 23 | -- 24 | 25 | CREATE TABLE IF NOT EXISTS `edge` ( 26 | `from_node_id` bigint(20) unsigned NOT NULL, 27 | `to_node_id` bigint(20) unsigned NOT NULL, 28 | `type` bigint(20) unsigned NOT NULL, 29 | `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 30 | `updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 31 | PRIMARY KEY (`from_node_id`,`to_node_id`,`type`), 32 | KEY `from_type_updated` (`from_node_id`,`type`,`updated`), 33 | KEY `to_node_id` (`to_node_id`) 34 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1; 35 | 36 | -- -------------------------------------------------------- 37 | 38 | -- 39 | -- Table structure for table `node` 40 | -- 41 | 42 | CREATE TABLE IF NOT EXISTS `node` ( 43 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 44 | `type` bigint(20) unsigned NOT NULL, 45 | `data` text COLLATE utf8_unicode_ci NOT NULL, 46 | `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 47 | `updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 48 | PRIMARY KEY (`id`), 49 | KEY `type_updated` (`type`,`updated`) 50 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=1000000; 51 | 52 | -- -------------------------------------------------------- 53 | 54 | -- 55 | -- Table structure for table `node_data` 56 | -- 57 | 58 | CREATE TABLE IF NOT EXISTS `node_data` ( 59 | `node_id` bigint(20) unsigned NOT NULL, 60 | `type` bigint(20) unsigned NOT NULL, 61 | `data` text COLLATE utf8_unicode_ci NOT NULL, 62 | `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 63 | `updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 64 | PRIMARY KEY (`node_id`,`type`), 65 | KEY `type_data` (`type`,`data`(128)), 66 | KEY `type_updated` (`type`,`updated`) 67 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 68 | 69 | -- 70 | -- Constraints for dumped tables 71 | -- 72 | 73 | -- 74 | -- Constraints for table `edge` 75 | -- 76 | ALTER TABLE `edge` 77 | ADD CONSTRAINT `edge_ibfk_2` FOREIGN KEY (`to_node_id`) REFERENCES `node` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, 78 | ADD CONSTRAINT `edge_ibfk_1` FOREIGN KEY (`from_node_id`) REFERENCES `node` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; 79 | 80 | -- 81 | -- Constraints for table `node_data` 82 | -- 83 | ALTER TABLE `node_data` 84 | ADD CONSTRAINT `node_data_ibfk_1` FOREIGN KEY (`node_id`) REFERENCES `node` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; 85 | 86 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 87 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 88 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 89 | -------------------------------------------------------------------------------- /graphp/lib/GPProfiler.php: -------------------------------------------------------------------------------- 1 | idx($_SERVER, 'REQUEST_URI', ''), 16 | 'name' => 'enabled', 17 | 'microtime' => microtime(true), 18 | ]; 19 | PhutilServiceProfiler::getInstance()->addListener('GPProfiler::format'); 20 | } 21 | 22 | public static function getQueryData() { 23 | return self::$queryData; 24 | } 25 | 26 | public static function getMarks() { 27 | return self::$marks; 28 | } 29 | 30 | public static function format($type, $id, $data) { 31 | $data['id'] = $id; 32 | $data['stage'] = $type; 33 | self::$queryData[] = $data; 34 | } 35 | 36 | public static function mark($name = '') { 37 | if (!self::$enabled) { 38 | return; 39 | } 40 | $name = $name ?: 'Mark '.count(self::$marks); 41 | self::$marks[] = ['name' => $name, 'microtime' => microtime(true)]; 42 | $count = count(self::$marks); 43 | self::$marks[$count - 1]['duration'] = 44 | self::$marks[$count - 1]['microtime'] - 45 | self::$marks[$count - 2]['microtime']; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /graphp/lib/GPRouteGenerator.php: -------------------------------------------------------------------------------- 1 | path = explode('/', parse_url($uri)['path']); 12 | } 13 | 14 | public static function createFromParts($controller, $method = 'index', ...$args) { 15 | $route_generator = new GPRouteGenerator(); 16 | $route_generator->controller = $controller; 17 | $route_generator->method = $method; 18 | $route_generator->args = $args; 19 | return $route_generator; 20 | } 21 | 22 | public function getPath() { 23 | return $this->path; 24 | } 25 | 26 | public function getController() { 27 | return $this->controller; 28 | } 29 | 30 | public function getMethod() { 31 | return $this->method; 32 | } 33 | 34 | public function getArgs() { 35 | return $this->args; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /graphp/model/GPBatch.php: -------------------------------------------------------------------------------- 1 | nodes = $nodes; 13 | $this->classes = array_unique(array_map( 14 | function($node) { return get_class($node); }, 15 | $nodes 16 | )); 17 | $this->lazy = $lazy; 18 | } 19 | 20 | public function delete() { 21 | GPNode::batchDelete($this->nodes); 22 | } 23 | 24 | public function save() { 25 | GPNode::batchSave($this->nodes); 26 | return $this; 27 | } 28 | 29 | public function __call($method, $args) { 30 | 31 | if (substr_compare($method, 'forceLoad', 0, 9) === 0) { 32 | if ($this->lazy) { 33 | $this->lazyForce = true; 34 | } 35 | return $this->handleLoad(mb_substr($method, 5), $args, true); 36 | } 37 | 38 | if (substr_compare($method, 'load', 0, 4) === 0) { 39 | return $this->handleLoad($method, $args); 40 | } 41 | 42 | throw new GPException( 43 | 'Method '.$method.' not found in '.get_called_class() 44 | ); 45 | } 46 | 47 | public function load() { 48 | Assert::true($this->lazy, 'Cannot call load on non lazy batch loader'); 49 | GPNode::batchLoadConnectedNodes( 50 | $this->nodes, 51 | $this->lazyEdges, 52 | $this->lazyForce 53 | ); 54 | $this->lazyEdges = []; 55 | $this->lazyForce = false; 56 | return $this; 57 | } 58 | 59 | public function getConnectedNodeCount(array $edges) { 60 | if (!$this->nodes) { 61 | return []; 62 | } 63 | $results = GPDatabase::get()->getConnectedNodeCount($this->nodes, $edges); 64 | foreach ($results as $from_node_id => $rows) { 65 | $results[$from_node_id] = ipull($rows, 'c', 'type'); 66 | } 67 | return $results; 68 | } 69 | 70 | private function handleLoad($method, $args, $force = false) { 71 | if (!$this->nodes) { 72 | return $this; 73 | } 74 | if (substr_compare($method, 'IDs', -3) === 0) { 75 | if ($this->lazy) { 76 | throw new GPException('Lazy ID loading is not supported'); 77 | } else { 78 | GPNode::batchLoadConnectedNodes( 79 | $this->nodes, 80 | $this->getEdges(mb_substr($method, 4, -3)), 81 | $force, 82 | true 83 | ); 84 | } 85 | } else { 86 | if ($this->lazy) { 87 | $this->lazyEdges += 88 | mpull($this->getEdges(mb_substr($method, 4)), null, 'getType'); 89 | } else { 90 | GPNode::batchLoadConnectedNodes( 91 | $this->nodes, 92 | $this->getEdges(mb_substr($method, 4)), 93 | $force 94 | ); 95 | } 96 | } 97 | return $this; 98 | } 99 | 100 | private function getEdges($edge_name) { 101 | $edges = array_filter(array_map( 102 | function($class) use ($edge_name) { 103 | return $class::isEdgeType($edge_name) ? 104 | $class::getEdgeType($edge_name) : 105 | null; 106 | }, 107 | $this->classes 108 | )); 109 | Assert::truthy($edges, $edge_name.' is not a valid edge name.'); 110 | return $edges; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /graphp/model/GPBatchLoader.php: -------------------------------------------------------------------------------- 1 | startTransaction(); 8 | foreach ($nodes as $node) { 9 | $node->save(); 10 | } 11 | $db->commit(); 12 | } 13 | 14 | public static function batchDelete(array $nodes) { 15 | $db = GPDatabase::get(); 16 | $db->startTransaction(); 17 | foreach ($nodes as $node) { 18 | $node->delete(); 19 | } 20 | $db->commit(); 21 | } 22 | 23 | /** 24 | * Deletes nodes, but ignores overriden delete() methods. More efficient but 25 | * won't do fancy recursive deletes. 26 | */ 27 | public static function simpleBatchDelete(array $nodes) { 28 | GPDatabase::get()->deleteNodes($nodes); 29 | array_unset_keys(self::$cache, mpull($nodes, 'getID')); 30 | } 31 | 32 | public static function batchLoadConnectedNodes( 33 | array $nodes, 34 | array $edge_types, 35 | $force = false, 36 | $ids_only = false 37 | ) { 38 | $nodes = mpull($nodes, null, 'getID'); 39 | $raw_edge_types = mpull($edge_types, 'getType'); 40 | if (!$force) { 41 | $names = mpull($edge_types, 'getName'); 42 | foreach ($nodes as $key => $node) { 43 | $valid_edge_types = array_select_keys( 44 | $node::getEdgeTypesByType(), 45 | $raw_edge_types 46 | ); 47 | if ($node->isLoaded($valid_edge_types)) { 48 | unset($nodes[$key]); 49 | } 50 | } 51 | } 52 | $ids = GPDatabase::get()->multiGetConnectedIDs($nodes, $raw_edge_types); 53 | if (!$ids_only) { 54 | $to_nodes = GPNode::multiGetByID(array_flatten($ids)); 55 | } 56 | foreach ($ids as $from_id => $type_ids) { 57 | $loaded_nodes_for_type = []; 58 | if (!$ids_only) { 59 | foreach ($type_ids as $edge_type => $ids_for_edge_type) { 60 | $loaded_nodes_for_type[$edge_type] = array_select_keys( 61 | $to_nodes, 62 | $ids_for_edge_type 63 | ); 64 | } 65 | } 66 | $nodes[$from_id]->connectedNodeIDs = 67 | array_merge_by_keys($nodes[$from_id]->connectedNodeIDs, $type_ids); 68 | if (!$ids_only) { 69 | $nodes[$from_id]->connectedNodes = array_merge_by_keys( 70 | $nodes[$from_id]->connectedNodes, 71 | $loaded_nodes_for_type 72 | ); 73 | } 74 | } 75 | foreach ($nodes as $id => $node) { 76 | $types_for_node = $node::getEdgeTypes(); 77 | foreach ($edge_types as $type) { 78 | if ( 79 | !array_key_exists($id, $ids) && 80 | !array_key_exists($type->getType(), $nodes[$id]->connectedNodes) && 81 | array_key_exists($type->getName(), $types_for_node) 82 | ) { 83 | $nodes[$id]->connectedNodeIDs[$type->getType()] = []; 84 | if (!$ids_only) { 85 | $nodes[$id]->connectedNodes[$type->getType()] = []; 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /graphp/model/GPDataType.php: -------------------------------------------------------------------------------- 1 | name = strtolower($name); 28 | $this->type = $type; 29 | $this->isIndexed = $is_indexed; 30 | $this->default = $default; 31 | } 32 | 33 | public function assertValueIsOfType($value) { 34 | self::assertConstValueExists($this->type); 35 | if ($this->type === self::GP_ANY) { 36 | return true; 37 | } 38 | if (!call_user_func($this->type, $value)) { 39 | throw new Exception( 40 | 'Value '.$value.' is not of type '.mb_substr($this->type, 3) 41 | ); 42 | } 43 | return true; 44 | } 45 | 46 | public function getName() { 47 | return $this->name; 48 | } 49 | 50 | public function getIndexedType() { 51 | Assert::true($this->isIndexed()); 52 | return STRUtils::to64BitInt($this->name); 53 | } 54 | 55 | public function isIndexed() { 56 | return $this->isIndexed; 57 | } 58 | 59 | public function getType() { 60 | return $this->type; 61 | } 62 | 63 | public function getDefault() { 64 | return $this->default; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /graphp/model/GPEdgeType.php: -------------------------------------------------------------------------------- 1 | to = $to; 15 | $this->name = $name ? $name : $to; 16 | $this->storageKey = $storage_key; 17 | } 18 | 19 | public function setFromClass($from_class) { 20 | $this->fromType = $from_class::getType(); 21 | } 22 | 23 | public function getType() { 24 | if (!$this->type) { 25 | $this->type = STRUtils::to64BitInt($this->getStorageKey()); 26 | } 27 | return $this->type; 28 | } 29 | 30 | public function getName() { 31 | return strtolower($this->name); 32 | } 33 | 34 | public function getTo() { 35 | return $this->to; 36 | } 37 | 38 | public function getToType() { 39 | $to = $this->to; 40 | return $to::getType(); 41 | } 42 | 43 | public function getFromType() { 44 | return $this->fromType; 45 | } 46 | 47 | public function setSingleNodeName($name) { 48 | $this->singleNodeName = $name; 49 | return $this; 50 | } 51 | 52 | public function getSingleNodeName() { 53 | return strtolower($this->singleNodeName); 54 | } 55 | 56 | public function inverse($inverse) { 57 | $this->inverse = $inverse; 58 | $inverse->inverse = $this; 59 | $inverse->fromType = $this->getToType(); 60 | return $this; 61 | } 62 | 63 | public function getInverse() { 64 | return $this->inverse; 65 | } 66 | 67 | private function getStorageKey() { 68 | if ($this->storageKey) { 69 | return $this->storageKey; 70 | } 71 | return $this->fromType.$this->getToType().$this->name; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /graphp/model/GPNode.php: -------------------------------------------------------------------------------- 1 | [array of nodes indexed by id]) 16 | $connectedNodeIDs = [], 17 | $connectedNodes = [], 18 | $pendingConnectedNodes = [], 19 | $pendingRemovalNodes = [], 20 | $pendingRemovalAllNodes = [], 21 | $pendingRemovalAllInverseNodes = [], 22 | $nodesWithInverse = []; 23 | 24 | protected static 25 | $data_types = [], 26 | $edge_types = [], 27 | $edge_types_by_type = []; 28 | 29 | public function __construct(array $data = []) { 30 | $this->data = []; 31 | foreach ($data as $key => $value) { 32 | $this->data[strtolower($key)] = $value; 33 | } 34 | } 35 | 36 | public function getID() { 37 | return (int) $this->id; 38 | } 39 | 40 | public function isSaved(): bool { 41 | return $this->getID() > 0; 42 | } 43 | 44 | public function getUpdated() { 45 | return $this->updated; 46 | } 47 | 48 | public function getCreatedTimestamp() { 49 | return $this->created; 50 | } 51 | 52 | public static function getType() { 53 | return STRUtils::to64BitInt(static::getStorageKey()); 54 | } 55 | 56 | public static function getStorageKey() { 57 | return 'node_'.get_called_class(); 58 | } 59 | 60 | public function setDataX($key, $value) { 61 | $data_type = self::getDataTypeByName($key); 62 | if ($data_type === null) { 63 | throw new GPException( 64 | '\''.$key.'\' is not a declared data type on '.get_class($this). 65 | '. Possible data types are ['. 66 | implode(', ', mpull(self::getDataTypes(), 'getName')).']' 67 | ); 68 | } 69 | $data_type->assertValueIsOfType($value); 70 | return $this->setData($key, $value); 71 | } 72 | 73 | public function setData($key, $value) { 74 | $this->data[strtolower($key)] = $value; 75 | return $this; 76 | } 77 | 78 | public function setDataArray(array $data): self { 79 | $this->data = $data; 80 | return $this; 81 | } 82 | 83 | public function getDataX($key) { 84 | $type = self::getDataTypeByName($key); 85 | Assert::truthy($type); 86 | return $this->getData($key, $type->getDefault()); 87 | } 88 | 89 | public function getData($key, $default = null) { 90 | return idx($this->data, strtolower($key), $default); 91 | } 92 | 93 | public function getDataArray() { 94 | return $this->data; 95 | } 96 | 97 | public function getJSONData() { 98 | return json_encode($this->data); 99 | } 100 | 101 | public function getIndexedData() { 102 | $keys = array_keys(mfilter(self::getDataTypes(), 'isIndexed')); 103 | return array_select_keys($this->data, $keys); 104 | } 105 | 106 | public function unsetDataX($key) { 107 | Assert::truthy(self::getDataTypeByName($key)); 108 | return $this->unsetData($key); 109 | } 110 | 111 | public function unsetData($key) { 112 | unset($this->data[strtolower($key)]); 113 | return $this; 114 | } 115 | 116 | public function unsafeSave() { 117 | GPDatabase::get()->beginUnguardedWrites(); 118 | $ret = $this->save(); 119 | GPDatabase::get()->endUnguardedWrites(); 120 | return $ret; 121 | } 122 | 123 | public function save() { 124 | $db = GPDatabase::get(); 125 | $db->startTransaction(); 126 | if ($this->id) { 127 | $db->updateNodeData($this); 128 | } else { 129 | $this->id = $db->insertNode($this); 130 | $this->updated = date('Y-m-d H-i-s'); 131 | self::$cache[$this->id] = $this; 132 | } 133 | $db->updateNodeIndexedData($this); 134 | $db->saveEdges($this, $this->pendingConnectedNodes); 135 | $db->deleteEdges($this, $this->pendingRemovalNodes); 136 | $db->deleteAllEdges($this, $this->pendingRemovalAllNodes); 137 | $db->deleteAllInverseEdges($this, $this->pendingRemovalAllInverseNodes); 138 | batch($this->nodesWithInverse)->save(); 139 | $db->commit(); 140 | $this->pendingConnectedNodes = []; 141 | $this->pendingRemovalNodes = []; 142 | $this->pendingRemovalAllNodes = []; 143 | $this->pendingRemovalAllInverseNodes = []; 144 | $this->nodesWithInverse = []; 145 | return $this; 146 | } 147 | 148 | public function addPendingConnectedNodes(GPEdgeType $edge, array $nodes) { 149 | if ($edge->getInverse()) { 150 | $this->nodesWithInverse += mpull($nodes, null, 'getID'); 151 | foreach ($nodes as $node) { 152 | $node->addPendingNodes( 153 | 'pendingConnectedNodes', 154 | $edge->getInverse(), 155 | [$this] 156 | ); 157 | } 158 | } 159 | return $this->addPendingNodes('pendingConnectedNodes', $edge, $nodes); 160 | } 161 | 162 | public function addPendingRemovalNodes(GPEdgeType $edge, array $nodes) { 163 | if ($edge->getInverse()) { 164 | $this->nodesWithInverse += mpull($nodes, null, 'getID'); 165 | foreach ($nodes as $key => $node) { 166 | $node->addPendingNodes( 167 | 'pendingRemovalNodes', 168 | $edge->getInverse(), 169 | [$this] 170 | ); 171 | } 172 | } 173 | return $this->addPendingNodes('pendingRemovalNodes', $edge, $nodes); 174 | } 175 | 176 | public function addPendingRemovalAllNodes($edge) { 177 | if ($edge->getInverse()) { 178 | // This can be slow :( there is no index on (to_node_id, type) 179 | $this->pendingRemovalAllInverseNodes[$edge->getInverse()->getType()] = 180 | $edge->getInverse()->getType(); 181 | } 182 | $this->pendingRemovalAllNodes[$edge->getType()] = $edge->getType(); 183 | return $this; 184 | } 185 | 186 | private function addPendingNodes($var, GPEdgeType $edge, array $nodes) { 187 | Assert::equals( 188 | count($nodes), 189 | count(mfilter(array_filter($nodes), 'getID')), 190 | 'You can\'t add nodes that have not been saved' 191 | ); 192 | Assert::allEquals( 193 | mpull($nodes, 'getType'), 194 | $edge->getToType(), 195 | 'Trying to add nodes of the wrong type. '. 196 | json_encode(mpull($nodes, 'getType')).' != '.$edge->getToType() 197 | ); 198 | if (!array_key_exists($edge->getType(), $this->$var)) { 199 | $this->{$var}[$edge->getType()] = []; 200 | } 201 | $this->{$var}[$edge->getType()] = array_merge_by_keys( 202 | $this->{$var}[$edge->getType()], 203 | mpull($nodes, null, 'getID') 204 | ); 205 | return $this; 206 | } 207 | 208 | private function loadConnectedIDs( 209 | array $edges, 210 | $force = false, 211 | $limit = null, 212 | $offset = 0 213 | ) { 214 | $types = mpull($edges, 'getType', 'getType'); 215 | if ($force) { 216 | array_unset_keys($this->connectedNodeIDs, $types); 217 | } 218 | $already_loaded = array_select_keys($this->connectedNodeIDs, $types); 219 | $to_load_types = array_diff_key($types, $this->connectedNodeIDs); 220 | $ids = GPDatabase::get()->getConnectedIDs( 221 | $this, 222 | $to_load_types, 223 | $limit, 224 | $offset); 225 | $this->connectedNodeIDs = array_merge_by_keys( 226 | $this->connectedNodeIDs, 227 | $ids + $already_loaded 228 | ); 229 | return $this; 230 | } 231 | 232 | private function getConnectedIDs(array $edges) { 233 | $types = mpull($edges, 'getType'); 234 | return array_select_keysx($this->connectedNodeIDs, $types); 235 | } 236 | 237 | public function loadConnectedNodes( 238 | array $edges, 239 | $force = false, 240 | $limit = null, 241 | $offset = 0 242 | ) { 243 | $ids = $this 244 | ->loadConnectedIDs($edges, $force, $limit, $offset) 245 | ->getConnectedIDs($edges); 246 | $nodes = GPNode::multiGetByID(array_flatten($ids)); 247 | foreach ($ids as $edge_type => & $ids_for_edge_type) { 248 | foreach ($ids_for_edge_type as $key => $id) { 249 | $ids_for_edge_type[$key] = $nodes[$id]; 250 | } 251 | } 252 | $this->connectedNodes = array_merge_by_keys($this->connectedNodes, $ids); 253 | return $this; 254 | } 255 | 256 | public function getConnectedNodes(array $edges) { 257 | $types = mpull($edges, 'getType'); 258 | return array_select_keysx( 259 | $this->connectedNodes, 260 | $types, 261 | function() use ($edges) { 262 | $ids = array_keys($this->connectedNodes); 263 | throw new GPException( 264 | GPErrorText::missingEdges($edges, $this, $ids), 265 | 1 266 | ); 267 | } 268 | ); 269 | } 270 | 271 | public function getConnectedNodeCount(array $edges) { 272 | $results = GPDatabase::get()->getConnectedNodeCount([$this], $edges); 273 | return ipull(idx($results, $this->getID(), []), 'c', 'type'); 274 | } 275 | 276 | private function isLoaded($edge_or_edges) { 277 | $edges = make_array($edge_or_edges); 278 | $types = mpull($edges, 'getType'); 279 | return 280 | count(array_select_keys($this->connectedNodes, $types)) === count($types); 281 | } 282 | 283 | protected static function getEdgeTypesImpl() { 284 | return []; 285 | } 286 | 287 | public function delete() { 288 | unset(self::$cache[$this->getID()]); 289 | GPDatabase::get()->deleteNodes([$this]); 290 | } 291 | 292 | final public static function getEdgeTypes() { 293 | $class = get_called_class(); 294 | if (!isset(static::$edge_types[$class])) { 295 | $edges = static::getEdgeTypesImpl(); 296 | foreach ($edges as $edge) { 297 | $edge->setFromClass(get_called_class()); 298 | } 299 | static::$edge_types[$class] = 300 | mpull($edges, null, 'getName') + 301 | mpull($edges, null, 'getSingleNodeName'); 302 | unset(static::$edge_types[$class]['']); 303 | static::$edge_types_by_type[$class] = mpull($edges, null, 'getType'); 304 | foreach ($edges as $edge) { 305 | if (!array_key_exists($edge->getTo(), static::$edge_types)) { 306 | $to = $edge->getTo(); 307 | $to::getEdgeTypes(); 308 | } 309 | } 310 | } 311 | return static::$edge_types[$class]; 312 | } 313 | 314 | final public static function getEdgeType($name) { 315 | $name = strtolower($name); 316 | if (!array_key_exists($name, self::getEdgeTypes())) { 317 | throw new GPException(GPErrorText::missingEdgeType( 318 | get_called_class(), 319 | $name, 320 | self::getEdgeTypes() 321 | )); 322 | } 323 | return self::getEdgeTypes()[$name]; 324 | } 325 | 326 | final public static function isEdgeType($name) { 327 | return (bool) idx(self::getEdgeTypes(), strtolower($name)); 328 | } 329 | 330 | final public static function getEdgeTypeByType($type) { 331 | $class = get_called_class(); 332 | isset(static::$edge_types_by_type[$class]) ?: self::getEdgeTypes(); 333 | return idxx(static::$edge_types_by_type[$class], $type); 334 | } 335 | 336 | final public static function getEdgeTypesByType() { 337 | self::getEdgeTypes(); 338 | return static::$edge_types_by_type[get_called_class()]; 339 | } 340 | 341 | protected static function getDataTypesImpl() { 342 | return []; 343 | } 344 | 345 | final public static function getDataTypes() { 346 | $class = get_called_class(); 347 | if (!array_key_exists($class, self::$data_types)) { 348 | self::$data_types[$class] = 349 | mpull(static::getDataTypesImpl(), null, 'getName'); 350 | } 351 | return self::$data_types[$class]; 352 | } 353 | 354 | final public static function getDataTypeByName($name) { 355 | $data_types = self::getDataTypes(); 356 | return idx($data_types, strtolower($name)); 357 | } 358 | } 359 | 360 | function batch(/*array of nodes, single nodes, arrays of arrays of nodes*/) { 361 | return new GPBatch(array_flatten(func_get_args()), false /*lazy*/); 362 | } 363 | 364 | function lazy(/*array of nodes, single nodes, arrays of arrays of nodes*/) { 365 | return new GPBatch(array_flatten(func_get_args()), true /*lazy*/); 366 | } 367 | -------------------------------------------------------------------------------- /graphp/model/GPNodeLoader.php: -------------------------------------------------------------------------------- 1 | id = $data['id']; 16 | $node->updated = $data['updated']; 17 | $node->created = $data['created']; 18 | return $node; 19 | } 20 | 21 | public static function getByID($id) { 22 | if (!array_key_exists($id, self::$cache)) { 23 | $node_data = get_called_class() === GPNode::class ? 24 | GPDatabase::get()->getNodeByID($id) : 25 | GPDatabase::get()->getNodeByIDType($id, self::getType()); 26 | if ($node_data === null) { 27 | return null; 28 | } 29 | self::$cache[$id] = self::nodeFromArray($node_data); 30 | } 31 | if ( 32 | get_called_class() !== GPNode::class && 33 | self::$cache[$id]->getType() !== self::getType() 34 | ) { 35 | return null; 36 | } 37 | return self::$cache[$id]; 38 | } 39 | 40 | public static function multiGetByID(array $ids) { 41 | $ids = key_by_value($ids); 42 | $to_fetch = array_diff_key($ids, self::$cache); 43 | $node_datas = get_called_class() === GPNode::class ? 44 | GPDatabase::get()->multigetNodeByIDType($to_fetch) : 45 | GPDatabase::get()->multigetNodeByIDType($to_fetch, self::getType()); 46 | foreach ($node_datas as $node_data) { 47 | self::$cache[$node_data['id']] = self::nodeFromArray($node_data); 48 | } 49 | $nodes = array_select_keys(self::$cache, $ids); 50 | if (get_called_class() === GPNode::class) { 51 | return $nodes; 52 | } 53 | $type = self::getType(); 54 | return array_filter( 55 | $nodes, 56 | function($node) use ($type) { return $node->getType() === $type; } 57 | ); 58 | } 59 | 60 | public static function __callStatic($name, array $arguments) { 61 | $only_one = false; 62 | $len = mb_strlen($name); 63 | if ( 64 | strpos($name, 'getBy') === 0 && 65 | strpos($name, 'Range') === ($len - 5) 66 | ) { 67 | $type_name = strtolower(mb_substr($name, 5, $len - 10)); 68 | $range = true; 69 | } else if (substr_compare($name, 'getBy', 0, 5) === 0) { 70 | $type_name = strtolower(mb_substr($name, 5)); 71 | } else if (substr_compare($name, 'getOneBy', 0, 8) === 0) { 72 | $type_name = strtolower(mb_substr($name, 8)); 73 | $only_one = true; 74 | } else if (substr_compare($name, 'getAllWith', 0, 10) === 0) { 75 | $type_name = strtolower(mb_substr($name, 10)); 76 | $get_all_with_type = true; 77 | } 78 | 79 | if (isset($type_name)) { 80 | $class = get_called_class(); 81 | $data_type = static::getDataTypeByName($type_name); 82 | Assert::truthy($data_type, $name.' is not a method on '.$class); 83 | if (isset($range)) { 84 | $required_args = 2; 85 | } else if (isset($get_all_with_type)) { 86 | $required_args = 0; 87 | } else { 88 | $required_args = 1; 89 | } 90 | Assert::truthy( 91 | count($arguments) >= $required_args, 92 | GPErrorText::wrongArgs($class, $name, $required_args, count($arguments)) 93 | ); 94 | if ($arguments) { 95 | $arg = idx0($arguments); 96 | foreach (make_array($arg) as $val) { 97 | $data_type->assertValueIsOfType($val); 98 | } 99 | } 100 | if (isset($range)) { 101 | array_unshift($arguments, $data_type->getIndexedType()); 102 | $results = self::getByIndexDataRange($arguments); 103 | } else if (isset($get_all_with_type)) { 104 | $results = self::getByIndexDataAll($data_type->getIndexedType()); 105 | } else { 106 | $results = self::getByIndexData($data_type->getIndexedType(), $arg); 107 | } 108 | return $only_one ? idx0($results) : $results; 109 | } 110 | throw new GPException('Method '.$name.' not found in '.get_called_class()); 111 | } 112 | 113 | public static function getAll($limit = GPDatabase::LIMIT, $offset = 0) { 114 | $node_datas = 115 | GPDatabase::get()->getAllByType(static::getType(), $limit, $offset); 116 | foreach ($node_datas as $node_data) { 117 | if (!isset(self::$cache[$node_data['id']])) { 118 | self::$cache[$node_data['id']] = self::nodeFromArray($node_data); 119 | } 120 | } 121 | return array_select_keys(self::$cache, ipull($node_datas, 'id')); 122 | } 123 | 124 | public static function clearCache() { 125 | self::$cache = []; 126 | } 127 | 128 | public static function unsetFromCache($id) { 129 | unset(self::$cache[$id]); 130 | } 131 | 132 | private static function getByIndexDataAll($data_type) { 133 | $node_ids = GPDatabase::get()->getNodeIDsByTypeDataAll($data_type); 134 | return self::multiGetByID($node_ids); 135 | } 136 | 137 | private static function getByIndexData($data_type, $data) { 138 | $db = GPDatabase::get(); 139 | $node_ids = $db->getNodeIDsByTypeData($data_type, make_array($data)); 140 | return self::multiGetByID($node_ids); 141 | } 142 | 143 | private static function getByIndexDataRange($args) { 144 | $ids = GPDatabase::get()->getNodeIDsByTypeDataRange( 145 | $args[0], 146 | $args[1], 147 | $args[2], 148 | idx($args, 3, GPDatabase::LIMIT), 149 | idx($args, 4, 0)); 150 | return self::multiGetByID($ids); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /graphp/model/GPNodeMagicMethods.php: -------------------------------------------------------------------------------- 1 | handleGet(mb_substr($method, 3), $args); 41 | } 42 | 43 | if (substr_compare($method, 'is', 0, 2) === 0) { 44 | $name = mb_substr($method, 2); 45 | $type = static::getDataTypeByName($name); 46 | if ($type && $type->getType() === GPDataType::GP_BOOL) { 47 | return $this->handleGet(mb_substr($method, 2), $args); 48 | } 49 | } 50 | 51 | if (substr_compare($method, 'set', 0, 3) === 0) { 52 | Assert::equals( 53 | count($args), 54 | 1, 55 | GPErrorText::wrongArgs(get_called_class(), $method, 1, count($args)) 56 | ); 57 | return $this->setDataX(strtolower(mb_substr($method, 3)), idx0($args)); 58 | } 59 | 60 | if (substr_compare($method, 'add', 0, 3) === 0) { 61 | $edge = static::getEdgeType(mb_substr($method, 3)); 62 | return $this->addPendingConnectedNodes($edge, make_array(idx0($args))); 63 | } 64 | 65 | if (substr_compare($method, 'removeAll', 0, 9) === 0) { 66 | $edge = static::getEdgeType(mb_substr($method, 9)); 67 | return $this->addPendingRemovalAllNodes($edge); 68 | } 69 | 70 | if (substr_compare($method, 'remove', 0, 6) === 0) { 71 | $edge = static::getEdgeType(mb_substr($method, 6)); 72 | return $this->addPendingRemovalNodes($edge, make_array(idx0($args))); 73 | } 74 | 75 | if (substr_compare($method, 'forceLoad', 0, 9) === 0) { 76 | return $this->handleLoad(mb_substr($method, 5), $args, true); 77 | } 78 | 79 | if (substr_compare($method, 'load', 0, 4) === 0) { 80 | return $this->handleLoad($method, $args); 81 | } 82 | 83 | if (substr_compare($method, 'unset', 0, 3) === 0) { 84 | return $this->unsetDataX(strtolower(mb_substr($method, 5))); 85 | } 86 | 87 | throw new GPException( 88 | 'Method '.$method.' not found in '.get_called_class() 89 | ); 90 | } 91 | 92 | private function handleGet($name, $args) { 93 | $lower_name = strtolower($name); 94 | // Default to checking data first. 95 | $type = static::getDataTypeByName($lower_name); 96 | if ($type) { 97 | self::$methodCache[get_class($this).'get'.$name] = 98 | function($that) use ($type, $lower_name) { 99 | return $that->getData($lower_name, $type->getDefault()); 100 | }; 101 | return $this->getData($lower_name, $type->getDefault()); 102 | } 103 | 104 | if (substr_compare($name, 'One', 0, 3, true) === 0) { 105 | $name = str_ireplace('One', '', $name); 106 | $one = true; 107 | } 108 | 109 | if (substr_compare($name, 'IDs', -3) === 0) { 110 | $name = str_ireplace('IDs', '', $name); 111 | $ids_only = true; 112 | } else if (substr_compare($name, 'ID', -2) === 0) { 113 | $name = str_ireplace('ID', '', $name); 114 | $ids_only = true; 115 | } 116 | 117 | $name = strtolower($name); 118 | 119 | $edge = static::getEdgeType($name); 120 | if ($edge) { 121 | if ($name === $edge->getSingleNodeName()) { 122 | $one = true; 123 | } 124 | if (empty($ids_only)) { 125 | $result = $this->getConnectedNodes([$edge]); 126 | } else { 127 | $result = $this->getConnectedIDs([$edge]); 128 | } 129 | $nodes = idx($result, $edge->getType(), []); 130 | return empty($one) ? $nodes : idx0($nodes); 131 | } else { 132 | throw new GPException( 133 | 'Getter did not match any data or edge', 134 | 1 135 | ); 136 | } 137 | } 138 | 139 | private function handleLoad($method, $args, $force = false) { 140 | $limit = null; 141 | $offset = 0; 142 | if (count($args) === 1) { 143 | $limit = idx0($args); 144 | } else if (count($args) === 2) { 145 | list($limit, $offset) = $args; 146 | } 147 | if (substr_compare($method, 'IDs', -3) === 0) { 148 | $edge_name = mb_substr($method, 4, -3); 149 | $edge = static::getEdgeType($edge_name); 150 | return $this->loadConnectedIDs([$edge], $force, $limit, $offset); 151 | } else { 152 | $edge_name = mb_substr($method, 4); 153 | $edge = static::getEdgeType($edge_name); 154 | return $this->loadConnectedNodes([$edge], $force, $limit, $offset); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /graphp/model/GPNodeMap.php: -------------------------------------------------------------------------------- 1 | app_folder; 61 | self::$map = []; 62 | self::$inverseMap = []; 63 | $file = "getAllFileNames() as $class) { 66 | $file .= ' '.$class::getType().' => \''.$class."',\n"; 67 | self::$map[$class::getType()] = $class; 68 | self::$inverseMap[$class] = $class::getType(); 69 | } 70 | $file .= "];\n"; 71 | $file_path = self::buildPath(); 72 | $does_file_exist = file_exists($file_path); 73 | file_put_contents($file_path, $file); 74 | if (!$does_file_exist) { 75 | // File was just created, make sure to make it readable 76 | chmod($file_path, 0666); 77 | } 78 | } 79 | 80 | private static function buildPath() { 81 | return '/tmp/maps/'.GPConfig::get()->app_folder.'_node'; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /graphp/model/traits/GPDataTypeCreator.php: -------------------------------------------------------------------------------- 1 | setSingleNodeName($defaulted_single_name); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /graphp/tests/GPBatchLoaderTest.php: -------------------------------------------------------------------------------- 1 | beginUnguardedWrites(); 43 | GPNodeMap::addToMapForTest(GPTestBatchLoaderModel::class); 44 | GPNodeMap::addToMapForTest(GPTestBatchLoaderModel2::class); 45 | GPNodeMap::addToMapForTest(GPTestBatchLoaderModel3::class); 46 | GPNodeMap::addToMapForTest(GPTestBatchLoaderModel4::class); 47 | } 48 | 49 | public function testBatchSave() { 50 | $m1 = new GPTestBatchLoaderModel(); 51 | $m2 = new GPTestBatchLoaderModel(); 52 | GPNode::batchSave([$m1, $m2]); 53 | $this->assertNotEmpty($m1->getID()); 54 | $this->assertNotEmpty($m2->getID()); 55 | } 56 | 57 | public function testBatchDelete() { 58 | GPTestBatchLoaderModel::$customDelete = false; 59 | $m1 = new GPTestBatchLoaderModel(); 60 | $m2 = new GPTestBatchLoaderModel(); 61 | GPNode::batchSave([$m1, $m2]); 62 | $this->assertNotEmpty($m1->getID()); 63 | $this->assertNotEmpty($m2->getID()); 64 | GPNode::batchDelete([$m1, $m2]); 65 | $results = GPNode::multiGetByID([$m1->getID(), $m2->getID()]); 66 | $this->assertEmpty($results); 67 | $this->assertTrue(GPTestBatchLoaderModel::$customDelete); 68 | } 69 | 70 | public function testSimpleBatchDelete() { 71 | GPTestBatchLoaderModel::$customDelete = false; 72 | $m1 = new GPTestBatchLoaderModel(); 73 | $m2 = new GPTestBatchLoaderModel(); 74 | GPNode::batchSave([$m1, $m2]); 75 | $this->assertNotEmpty($m1->getID()); 76 | $this->assertNotEmpty($m2->getID()); 77 | GPNode::simpleBatchDelete([$m1, $m2]); 78 | $results = GPNode::multiGetByID([$m1->getID(), $m2->getID()]); 79 | $this->assertEmpty($results); 80 | $this->assertFalse(GPTestBatchLoaderModel::$customDelete); 81 | } 82 | 83 | public function testBatchLoad() { 84 | $m1 = new GPTestBatchLoaderModel(); 85 | $m2 = new GPTestBatchLoaderModel2(); 86 | $m3 = new GPTestBatchLoaderModel3(); 87 | $m4 = new GPTestBatchLoaderModel4(); 88 | GPNode::batchSave([$m1, $m2, $m3, $m4]); 89 | $m1->addGPTestBatchLoaderModel2($m2); 90 | $m2->addGPTestBatchLoaderModel3($m3); 91 | $m2->addGPTestBatchLoaderModel4($m4); 92 | GPNode::batchSave([$m1, $m2]); 93 | GPNode::batchLoadConnectedNodes( 94 | [$m1, $m2, $m3], 95 | array_merge( 96 | GPTestBatchLoaderModel::getEdgeTypes(), 97 | GPTestBatchLoaderModel2::getEdgeTypes() 98 | ) 99 | ); 100 | $this->assertNotEmpty($m1->getGPTestBatchLoaderModel2()); 101 | $this->assertNotEmpty($m2->getGPTestBatchLoaderModel3()); 102 | $this->assertNotEmpty($m2->getGPTestBatchLoaderModel4()); 103 | } 104 | 105 | public static function tearDownAfterClass(): void { 106 | GPNode::simpleBatchDelete(GPTestBatchLoaderModel::getAll()); 107 | GPNode::simpleBatchDelete(GPTestBatchLoaderModel2::getAll()); 108 | GPNode::simpleBatchDelete(GPTestBatchLoaderModel3::getAll()); 109 | GPNode::simpleBatchDelete(GPTestBatchLoaderModel4::getAll()); 110 | GPDatabase::get()->endUnguardedWrites(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /graphp/tests/GPBatchTest.php: -------------------------------------------------------------------------------- 1 | beginUnguardedWrites(); 43 | GPNodeMap::addToMapForTest(GPTestBatchModel::class); 44 | GPNodeMap::addToMapForTest(GPTestBatchModel2::class); 45 | GPNodeMap::addToMapForTest(GPTestBatchModel3::class); 46 | GPNodeMap::addToMapForTest(GPTestBatchModel4::class); 47 | } 48 | 49 | public function testBatchSave() { 50 | $m1 = new GPTestBatchModel(); 51 | $m2 = new GPTestBatchModel(); 52 | batch($m1, $m2)->save(); 53 | $this->assertNotEmpty($m1->getID()); 54 | $this->assertNotEmpty($m2->getID()); 55 | } 56 | 57 | public function testBatchDelete() { 58 | GPTestBatchModel::$customDelete = false; 59 | $m1 = new GPTestBatchModel(); 60 | $m2 = new GPTestBatchModel(); 61 | batch($m1, $m2)->save(); 62 | $this->assertNotEmpty($m1->getID()); 63 | $this->assertNotEmpty($m2->getID()); 64 | batch($m1, $m2)->delete(); 65 | $results = GPNode::multiGetByID([$m1->getID(), $m2->getID()]); 66 | $this->assertEmpty($results); 67 | $this->assertTrue(GPTestBatchModel::$customDelete); 68 | } 69 | 70 | public function testSimpleBatchDelete() { 71 | GPTestBatchModel::$customDelete = false; 72 | $m1 = new GPTestBatchModel(); 73 | $m2 = new GPTestBatchModel(); 74 | batch([$m1, $m2])->save(); 75 | $this->assertNotEmpty($m1->getID()); 76 | $this->assertNotEmpty($m2->getID()); 77 | GPNode::simpleBatchDelete([$m1, $m2]); 78 | $results = GPNode::multiGetByID([$m1->getID(), $m2->getID()]); 79 | $this->assertEmpty($results); 80 | $this->assertFalse(GPTestBatchModel::$customDelete); 81 | } 82 | 83 | public function testBatchLoad() { 84 | $m1 = new GPTestBatchModel(); 85 | $m2 = new GPTestBatchModel2(); 86 | $m3 = new GPTestBatchModel3(); 87 | $m4 = new GPTestBatchModel4(); 88 | batch($m1, $m2, $m3, $m4)->save(); 89 | $m1->addGPTestBatchModel2($m2); 90 | $m2->addGPTestBatchModel3($m3); 91 | $m2->addGPTestBatchModel4($m4); 92 | batch($m1, $m2)->save(); 93 | batch($m1, $m2, $m3) 94 | ->loadGPTestBatchModel2() 95 | ->loadGPTestBatchModel3() 96 | ->loadGPTestBatchModel4(); 97 | $this->assertNotEmpty($m1->getGPTestBatchModel2()); 98 | $this->assertNotEmpty($m2->getGPTestBatchModel3()); 99 | $this->assertNotEmpty($m2->getGPTestBatchModel4()); 100 | } 101 | 102 | public function testBatchLazyLoad() { 103 | $m1 = new GPTestBatchModel(); 104 | $m2 = new GPTestBatchModel2(); 105 | $m3 = new GPTestBatchModel3(); 106 | $m4 = new GPTestBatchModel4(); 107 | batch($m1, $m2, $m3, $m4)->save(); 108 | $m1->addGPTestBatchModel2($m2); 109 | $m2->addGPTestBatchModel3($m3); 110 | $m2->addGPTestBatchModel4($m4); 111 | batch($m1, $m2)->save(); 112 | lazy($m1, $m2, $m3) 113 | ->loadGPTestBatchModel2() 114 | ->loadGPTestBatchModel3() 115 | ->loadGPTestBatchModel4() 116 | ->load(); 117 | $this->assertNotEmpty($m1->getGPTestBatchModel2()); 118 | $this->assertNotEmpty($m2->getGPTestBatchModel3()); 119 | $this->assertNotEmpty($m2->getGPTestBatchModel4()); 120 | } 121 | 122 | public function testErrorBatchLoad() { 123 | $this->expectException(GPException::class); 124 | 125 | $m1 = new GPTestBatchModel(); 126 | $m2 = new GPTestBatchModel2(); 127 | batch($m1, $m2)->loadBogus(); 128 | } 129 | 130 | public static function tearDownAfterClass(): void { 131 | GPNode::simpleBatchDelete(GPTestBatchModel::getAll()); 132 | GPNode::simpleBatchDelete(GPTestBatchModel2::getAll()); 133 | GPNode::simpleBatchDelete(GPTestBatchModel3::getAll()); 134 | GPNode::simpleBatchDelete(GPTestBatchModel4::getAll()); 135 | GPDatabase::get()->endUnguardedWrites(); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /graphp/tests/GPControllerHandlerTest.php: -------------------------------------------------------------------------------- 1 | use_index_php ? '/index.php' : ''; 14 | $uri = TestController::URI()->foo('abc'); 15 | $this->assertEquals($uri, $index.'/testcontroller/foo/abc'); 16 | } 17 | 18 | public function testShortURI() { 19 | $index = GPConfig::get()->use_index_php ? '/index.php' : ''; 20 | $uri = TestController::URI(); 21 | $this->assertEquals($uri, $index.'/testcontroller/'); 22 | } 23 | 24 | public function testBadURI() { 25 | $this->expectException(GPException::class); 26 | 27 | $uri = TestController::URI()->bar('abc'); 28 | } 29 | 30 | public function testURL() { 31 | $index = GPConfig::get()->use_index_php ? '/index.php' : ''; 32 | $domain = GPConfig::get()->domain; 33 | $uri = TestController::URL()->foo('abc'); 34 | $this->assertEquals($uri, $domain.$index.'/testcontroller/foo/abc'); 35 | } 36 | 37 | public function testShortURL() { 38 | $index = GPConfig::get()->use_index_php ? '/index.php' : ''; 39 | $domain = GPConfig::get()->domain; 40 | $uri = TestController::URL(); 41 | $this->assertEquals($uri, $domain.$index.'/testcontroller/'); 42 | } 43 | 44 | public function testRedirect() { 45 | $handler = TestController::redirect(); 46 | $this->assertTrue($handler instanceof GPRedirectControllerHandler); 47 | $handler->disableDestructRedirect(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /graphp/tests/GPEdgeCountTest.php: -------------------------------------------------------------------------------- 1 | setSingleNodeName('Foo'), 8 | ]; 9 | } 10 | } 11 | 12 | class GPTestCountModel2 extends GPNode { 13 | 14 | } 15 | 16 | class GPEdgeCountTest extends GPTest { 17 | 18 | public static function setUpBeforeClass(): void { 19 | GPDatabase::get()->beginUnguardedWrites(); 20 | GPNodeMap::addToMapForTest(GPTestCountModel1::class); 21 | GPNodeMap::addToMapForTest(GPTestCountModel2::class); 22 | } 23 | 24 | public function testCorrectCount() { 25 | $nodes = []; 26 | foreach (range(1, 10) as $key => $value) { 27 | $nodes[] = (new GPTestCountModel2())->save(); 28 | } 29 | $n1 = (new GPTestCountModel1())->save(); 30 | $n1->addGPTestCountModel2($nodes)->save(); 31 | $count = $n1->getConnectedNodeCount( 32 | [GPTestCountModel1::getEdgeType(GPTestCountModel2::class)]); 33 | $this->assertEquals(idx0($count), 10); 34 | } 35 | 36 | public function testBatchCorrectCount() { 37 | $nodes = []; 38 | foreach (range(1, 10) as $key => $value) { 39 | $nodes[] = (new GPTestCountModel2())->save(); 40 | } 41 | $n1 = (new GPTestCountModel1())->save(); 42 | $n12 = (new GPTestCountModel1())->save(); 43 | $n1->addGPTestCountModel2($nodes)->save(); 44 | $n12->addGPTestCountModel2(array_slice($nodes, 5))->save(); 45 | $count = batch($n1, $n12)->getConnectedNodeCount( 46 | [GPTestCountModel1::getEdgeType(GPTestCountModel2::class)]); 47 | $this->assertEquals(idx0($count[$n1->getID()]), 10); 48 | $this->assertEquals(idx0($count[$n12->getID()]), 5); 49 | } 50 | 51 | public static function tearDownAfterClass(): void { 52 | GPNode::simpleBatchDelete(GPTestCountModel1::getAll()); 53 | GPNode::simpleBatchDelete(GPTestCountModel2::getAll()); 54 | GPDatabase::get()->endUnguardedWrites(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /graphp/tests/GPEdgeInverseTest.php: -------------------------------------------------------------------------------- 1 | inverse(GPEdgeInverseTestModel2::getEdgeType( 9 | GPEdgeInverseTestModel1::class 10 | )), 11 | ]; 12 | } 13 | } 14 | 15 | class GPEdgeInverseTestModel2 extends GPNode { 16 | 17 | protected static function getEdgeTypesImpl() { 18 | return [ 19 | (new GPEdgeType(GPEdgeInverseTestModel1::class)), 20 | ]; 21 | } 22 | } 23 | 24 | class GPEdgeInverseTest extends GPTest { 25 | 26 | public static function setUpBeforeClass(): void { 27 | GPDatabase::get()->beginUnguardedWrites(); 28 | GPNodeMap::addToMapForTest(GPEdgeInverseTestModel1::class); 29 | GPNodeMap::addToMapForTest(GPEdgeInverseTestModel2::class); 30 | } 31 | 32 | public function testAddingAndGetting() { 33 | $model1 = (new GPEdgeInverseTestModel1())->save(); 34 | $model2 = (new GPEdgeInverseTestModel2())->save(); 35 | $model1->addGPEdgeInverseTestModel2($model2)->save(); 36 | $model2->loadGPEdgeInverseTestModel1(); 37 | $this->assertEquals($model2->getOneGPEdgeInverseTestModel1(), $model1); 38 | $this->assertEquals( 39 | $model2->getGPEdgeInverseTestModel1(), 40 | [$model1->getID() => $model1] 41 | ); 42 | } 43 | 44 | public function testEdgeRemoval() { 45 | $model1 = (new GPEdgeInverseTestModel1())->save(); 46 | $model2 = (new GPEdgeInverseTestModel2())->save(); 47 | $model3 = (new GPEdgeInverseTestModel2())->save(); 48 | $model1->addGPEdgeInverseTestModel2([$model2, $model3])->save(); 49 | $model1->loadGPEdgeInverseTestModel2(); 50 | $model1->removeGPEdgeInverseTestModel2($model2)->save(); 51 | // force loading should reset the edges 52 | $model2->forceLoadGPEdgeInverseTestModel1(); 53 | $model3->forceLoadGPEdgeInverseTestModel1(); 54 | $this->assertEquals($model2->getGPEdgeInverseTestModel1(), []); 55 | $this->assertEquals( 56 | $model3->getGPEdgeInverseTestModel1(), 57 | [$model1->getID() => $model1] 58 | ); 59 | } 60 | 61 | public function testAllEdgeRemoval() { 62 | $model1 = (new GPEdgeInverseTestModel1())->save(); 63 | $model2 = (new GPEdgeInverseTestModel2())->save(); 64 | $model3 = (new GPEdgeInverseTestModel2())->save(); 65 | $model1->addGPEdgeInverseTestModel2([$model2, $model3])->save(); 66 | $model1->loadGPEdgeInverseTestModel2(); 67 | $model1->removeAllGPEdgeInverseTestModel2()->save(); 68 | // force loading should reset the edges 69 | $model1->forceLoadGPEdgeInverseTestModel2(); 70 | $model2->forceLoadGPEdgeInverseTestModel1(); 71 | $model3->forceLoadGPEdgeInverseTestModel1(); 72 | $this->assertEmpty($model1->getGPEdgeInverseTestModel2()); 73 | $this->assertEmpty($model2->getGPEdgeInverseTestModel1()); 74 | $this->assertEmpty($model3->getGPEdgeInverseTestModel1()); 75 | } 76 | 77 | public static function tearDownAfterClass(): void { 78 | GPNode::simpleBatchDelete(GPEdgeInverseTestModel1::getAll()); 79 | GPNode::simpleBatchDelete(GPEdgeInverseTestModel2::getAll()); 80 | GPDatabase::get()->endUnguardedWrites(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /graphp/tests/GPEdgeTest.php: -------------------------------------------------------------------------------- 1 | setSingleNodeName('Foo'), 8 | ]; 9 | } 10 | } 11 | 12 | class GPTestModel2 extends GPNode { 13 | 14 | } 15 | 16 | class GPEdgeTest extends GPTest { 17 | 18 | public static function setUpBeforeClass(): void { 19 | GPDatabase::get()->beginUnguardedWrites(); 20 | GPNodeMap::addToMapForTest(GPTestModel1::class); 21 | GPNodeMap::addToMapForTest(GPTestModel2::class); 22 | } 23 | 24 | public function testAddingAndGetting() { 25 | $model1 = (new GPTestModel1())->save(); 26 | $model2 = (new GPTestModel2())->save(); 27 | $model1->addGPTestModel2($model2)->save(); 28 | $model1->loadGPTestModel2(); 29 | $this->assertEquals($model1->getOneGPTestModel2(), $model2); 30 | $this->assertEquals( 31 | $model1->getGPTestModel2(), 32 | [$model2->getID() => $model2] 33 | ); 34 | } 35 | 36 | public function testAddingAndGettingSingle() { 37 | $model1 = (new GPTestModel1())->save(); 38 | $model2 = (new GPTestModel2())->save(); 39 | $model1->addGPTestModel2($model2)->save(); 40 | $model1->loadGPTestModel2(); 41 | $this->assertEquals($model1->getFoo(), $model2); 42 | $this->assertEquals( 43 | $model1->getGPTestModel2(), 44 | [$model2->getID() => $model2] 45 | ); 46 | } 47 | 48 | public function testAddingWrongType() { 49 | $this->expectException(GPException::class); 50 | 51 | $model1 = (new GPTestModel1())->save(); 52 | $model2 = (new GPTestModel1())->save(); 53 | $model1->addGPTestModel2($model2)->save(); 54 | } 55 | 56 | public function testAddngBeforeSaving() { 57 | $this->expectException(GPException::class); 58 | 59 | $model1 = (new GPTestModel1())->save(); 60 | $model2 = new GPTestModel2(); 61 | $model1->addGPTestModel2($model2)->save(); 62 | } 63 | 64 | public function testAddingAndGettingIDs() { 65 | $model1 = (new GPTestModel1())->save(); 66 | $model2 = (new GPTestModel2())->save(); 67 | $model1->addGPTestModel2($model2)->save(); 68 | $model1->loadGPTestModel2IDs(); 69 | $this->assertEquals($model1->getOneGPTestModel2IDs(), $model2->getID()); 70 | $this->assertEquals( 71 | $model1->getGPTestModel2IDs(), 72 | [$model2->getID() => $model2->getID()] 73 | ); 74 | } 75 | 76 | public function testEdgeRemoval() { 77 | $model1 = (new GPTestModel1())->save(); 78 | $model2 = (new GPTestModel2())->save(); 79 | $model3 = (new GPTestModel2())->save(); 80 | $model1->addGPTestModel2([$model2, $model3])->save(); 81 | $model1->loadGPTestModel2(); 82 | $model1->removeGPTestModel2($model2)->save(); 83 | // force loading should reset the edges 84 | $model1->forceLoadGPTestModel2(); 85 | $this->assertEquals( 86 | $model1->getGPTestModel2(), 87 | [$model3->getID() => $model3] 88 | ); 89 | } 90 | 91 | public function testAllEdgeRemoval() { 92 | $model1 = (new GPTestModel1())->save(); 93 | $model2 = (new GPTestModel2())->save(); 94 | $model3 = (new GPTestModel2())->save(); 95 | $model1->addGPTestModel2([$model2, $model3])->save(); 96 | $model1->loadGPTestModel2(); 97 | $model1->removeAllGPTestModel2()->save(); 98 | // force loading should reset the edges 99 | $model1->forceLoadGPTestModel2(); 100 | $this->assertEmpty($model1->getGPTestModel2()); 101 | } 102 | 103 | public static function tearDownAfterClass(): void { 104 | GPNode::simpleBatchDelete(GPTestModel1::getAll()); 105 | GPNode::simpleBatchDelete(GPTestModel2::getAll()); 106 | GPDatabase::get()->endUnguardedWrites(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /graphp/tests/GPLoadByRangeTest.php: -------------------------------------------------------------------------------- 1 | beginUnguardedWrites(); 18 | GPNodeMap::addToMapForTest(GPTestRangeModel::class); 19 | } 20 | 21 | public function testLoadByNameRange() { 22 | $m1 = (new GPTestRangeModel())->setfirstName('Andres')->save(); 23 | $m2 = (new GPTestRangeModel())->setfirstName('Bibi')->save(); 24 | $m3 = (new GPTestRangeModel())->setfirstName('Charlotte')->save(); 25 | $results = GPTestRangeModel::getByFirstNameRange('Andres', 'zzz'); 26 | $this->assertEquals(array_values($results), [$m1, $m2, $m3]); 27 | $results = GPTestRangeModel::getByFirstNameRange('Bibis', 'zzz'); 28 | $this->assertEquals(array_values($results), [$m3]); 29 | } 30 | 31 | public function testLoadByAgeRangeWithLimit() { 32 | $m1 = (new GPTestRangeModel())->setAge(50)->save(); 33 | $m2 = (new GPTestRangeModel())->setAge(26)->save(); 34 | $m3 = (new GPTestRangeModel())->setAge(22)->save(); 35 | $results = GPTestRangeModel::getByAgeRange(22, 50, 1); 36 | $this->assertEquals(array_values($results), [$m1]); 37 | $results = GPTestRangeModel::getByAgeRange(22, 50, 1, 1); 38 | $this->assertEquals(array_values($results), [$m2]); 39 | $results = GPTestRangeModel::getByAgeRange(22, 50, 3, 0); 40 | $this->assertEquals(array_values($results), [$m1, $m2, $m3]); 41 | } 42 | 43 | public function testgetAllWith() { 44 | $m1 = (new GPTestRangeModel())->setSex('M')->save(); 45 | $m2 = (new GPTestRangeModel())->save(); 46 | $m3 = (new GPTestRangeModel())->setSex('F')->save(); 47 | $results = GPTestRangeModel::getAllWithSex(); 48 | $this->assertEquals(array_values($results), [$m1, $m3]); 49 | } 50 | 51 | public static function tearDownAfterClass(): void { 52 | batch(GPTestRangeModel::getAll())->delete(); 53 | GPDatabase::get()->endUnguardedWrites(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /graphp/tests/GPModelTest.php: -------------------------------------------------------------------------------- 1 | beginUnguardedWrites(); 20 | GPNodeMap::addToMapForTest(GPTestModel::class); 21 | } 22 | 23 | public function testCreate() { 24 | $model = new GPTestModel(); 25 | $this->assertEmpty($model->getID()); 26 | $model->save(); 27 | $this->assertNotEmpty($model->getID()); 28 | } 29 | 30 | public function testLoadByID() { 31 | $model = new GPTestModel(); 32 | $this->assertEmpty($model->getID()); 33 | $model->save(); 34 | $model::clearCache(); 35 | $this->assertNotEmpty(GPTestModel::getByID($model->getID())); 36 | // And from cache 37 | $this->assertNotEmpty(GPTestModel::getByID($model->getID())); 38 | } 39 | 40 | public function testLoadByIDWrongModel() { 41 | $model = new GPTestModel(); 42 | $this->assertEmpty($model->getID()); 43 | $model->save(); 44 | $model::clearCache(); 45 | $this->assertEmpty(GPTestOtherModel::getByID($model->getID())); 46 | // And from cache 47 | GPTestModel::getByID($model->getID()); 48 | $this->assertEmpty(GPTestOtherModel::getByID($model->getID())); 49 | } 50 | 51 | public function testMultiLoadByID() { 52 | $model = new GPTestModel(); 53 | $model2 = new GPTestModel(); 54 | batch($model, $model2)->save(); 55 | $model->save(); 56 | $model::clearCache(); 57 | $this->assertEquals( 58 | mpull( 59 | GPTestModel::multiGetByID([$model->getID(), $model2->getID()]), 60 | 'getID' 61 | ), 62 | mpull([$model, $model2], 'getID', 'getID') 63 | ); 64 | // And from cache 65 | $this->assertEquals( 66 | mpull( 67 | GPTestModel::multiGetByID([$model->getID(), $model2->getID()]), 68 | 'getID' 69 | ), 70 | mpull([$model, $model2], 'getID', 'getID') 71 | ); 72 | } 73 | 74 | public function testMultiLoadByIDWrongModel() { 75 | $model = new GPTestModel(); 76 | $model2 = new GPTestModel(); 77 | batch($model, $model2)->save(); 78 | $model->save(); 79 | $model::clearCache(); 80 | $this->assertEmpty( 81 | GPTestOtherModel::multiGetByID([$model->getID(), $model2->getID()]) 82 | ); 83 | // And from cache 84 | GPTestModel::getByID($model->getID()); 85 | $this->assertEmpty( 86 | GPTestOtherModel::multiGetByID([$model->getID(), $model2->getID()]) 87 | ); 88 | $this->assertNotEmpty(GPTestModel::multiGetByID([$model->getID()])); 89 | } 90 | 91 | public function testLoadByName() { 92 | $name = 'Weirds Name'; 93 | $model = new GPTestModel(['name' => $name]); 94 | $this->assertEmpty($model->getID()); 95 | $model->save(); 96 | $model::clearCache(); 97 | $this->assertNotEmpty(GPTestModel::getByName($name)); 98 | // From cache 99 | $this->assertNotEmpty(GPTestModel::getByName($name)); 100 | } 101 | 102 | public function testLoadByNameAfterUnset() { 103 | $name = 'Weirderer Name'; 104 | $model = new GPTestModel(['name' => $name]); 105 | $this->assertEmpty($model->getID()); 106 | $model->save(); 107 | $model::clearCache(); 108 | $loaded_model = GPTestModel::getOneByName($name); 109 | $this->assertNotEmpty($loaded_model); 110 | $loaded_model->unsetName(); 111 | $loaded_model->save(); 112 | $model::clearCache(); 113 | $this->assertEmpty(GPTestModel::getOneByName($name)); 114 | } 115 | 116 | public function testLoadByAge() { 117 | $this->expectException(GPException::class); 118 | 119 | $model = new GPTestModel(['name' => 'name', 'age' => 18]); 120 | $this->assertEmpty($model->getID()); 121 | $model->save(); 122 | GPTestModel::getByAge(18); 123 | } 124 | 125 | public function testGetData() { 126 | $model = new GPTestModel(['name' => 'Foo', 'age' => 18]); 127 | $this->assertEquals($model->getName(), 'Foo'); 128 | $this->assertEquals($model->getAge(), 18); 129 | $model->save(); 130 | $model::clearCache(); 131 | $loaded_model = GPTestModel::getByID($model->getID()); 132 | $this->assertEquals( 133 | $model->getDataArray(), 134 | ['name' => 'Foo', 'age' => 18] 135 | ); 136 | } 137 | 138 | public function testSetData() { 139 | $model = new GPTestModel(); 140 | $model->setName('Bar'); 141 | $model->setAge(25); 142 | $this->assertEquals( 143 | $model->getDataArray(), 144 | ['name' => 'Bar', 'age' => 25] 145 | ); 146 | } 147 | 148 | public function testDefaultData() { 149 | $model = new GPTestModel(); 150 | $this->assertEquals($model->getCurrency(), 'USD'); 151 | $this->assertNull($model->getAge()); 152 | } 153 | 154 | public static function tearDownAfterClass(): void { 155 | GPNode::simpleBatchDelete(GPTestModel::getAll()); 156 | GPDatabase::get()->endUnguardedWrites(); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /graphp/tests/GPTestLimitLoadTest.php: -------------------------------------------------------------------------------- 1 | beginUnguardedWrites(); 18 | GPNodeMap::addToMapForTest(GPTestLimitLoadModel::class); 19 | GPNodeMap::addToMapForTest(GPTestLimitLoadModel2::class); 20 | } 21 | 22 | public function testLimitLoad() { 23 | $m1 = new GPTestLimitLoadModel(); 24 | $m21 = new GPTestLimitLoadModel2(); 25 | $m22 = new GPTestLimitLoadModel2(); 26 | $m23 = new GPTestLimitLoadModel2(); 27 | batch($m1, $m21, $m22, $m23)->save(); 28 | $m1->addGPTestLimitLoadModel2([$m21, $m22, $m23])->save(); 29 | $m1->loadGPTestLimitLoadModel2(1); 30 | $this->assertEquals(count($m1->getGPTestLimitLoadModel2()), 1); 31 | $m1->forceLoadGPTestLimitLoadModel2(); 32 | $this->assertEquals(count($m1->getGPTestLimitLoadModel2()), 3); 33 | } 34 | 35 | public function testLimitLoadIDs() { 36 | $m1 = new GPTestLimitLoadModel(); 37 | $m21 = new GPTestLimitLoadModel2(); 38 | $m22 = new GPTestLimitLoadModel2(); 39 | $m23 = new GPTestLimitLoadModel2(); 40 | batch($m1, $m21, $m22, $m23)->save(); 41 | $m1->addGPTestLimitLoadModel2([$m21, $m22, $m23])->save(); 42 | $m1->loadGPTestLimitLoadModel2IDs(1); 43 | $this->assertEquals(count($m1->getGPTestLimitLoadModel2IDs()), 1); 44 | } 45 | 46 | public function testLimitOffsetLoad() { 47 | $m1 = new GPTestLimitLoadModel(); 48 | $m21 = new GPTestLimitLoadModel2(); 49 | $m22 = new GPTestLimitLoadModel2(); 50 | $m23 = new GPTestLimitLoadModel2(); 51 | batch($m1, $m21, $m22, $m23)->save(); 52 | $m1->addGPTestLimitLoadModel2([$m21, $m22, $m23])->save(); 53 | $m1->loadGPTestLimitLoadModel2(2, 1); 54 | $this->assertEquals(count($m1->getGPTestLimitLoadModel2()), 2); 55 | $this->assertEquals(idx0($m1->getGPTestLimitLoadModel2()), $m22); 56 | $this->assertEquals(last($m1->getGPTestLimitLoadModel2()), $m21); 57 | $m1->forceLoadGPTestLimitLoadModel2(); 58 | $this->assertEquals(count($m1->getGPTestLimitLoadModel2()), 3); 59 | } 60 | 61 | public static function tearDownAfterClass(): void { 62 | batch(GPTestLimitLoadModel::getAll())->delete(); 63 | batch(GPTestLimitLoadModel2::getAll())->delete(); 64 | GPDatabase::get()->endUnguardedWrites(); 65 | GPDatabase::get()->dispose(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /graphp/tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | '; 16 | echo $e->getMessage().'
'; 17 | echo str_replace("\n", '
', $e->getTraceAsString()).'

'; 18 | // Propagate exception so that it gets logged 19 | throw $e; 20 | } 21 | -------------------------------------------------------------------------------- /graphp/tests/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DIR=$(dirname "$0") 4 | cd $DIR 5 | ../../third_party/vendor/bin/phpunit --bootstrap bootstrap.php . 6 | -------------------------------------------------------------------------------- /graphp/utils/Assert.php: -------------------------------------------------------------------------------- 1 | = 0 && 33 | strpos($haystack, $needle, $temp) !== false 34 | ); 35 | } 36 | 37 | public static function pluralize(string $singular, string $plural = null) { 38 | if ($plural !== null) { 39 | return $plural; 40 | } 41 | 42 | $last_letter = strtolower($singular[strlen($singular) - 1]); 43 | switch ($last_letter) { 44 | case 'y': 45 | return substr($singular, 0, -1).'ies'; 46 | case 's': 47 | return $singular.'es'; 48 | default: 49 | return $singular.'s'; 50 | } 51 | } 52 | } 53 | 54 | class_alias('STRUtils', 'STR'); 55 | -------------------------------------------------------------------------------- /graphp/utils/arrays.php: -------------------------------------------------------------------------------- 1 | $value) { 38 | $result[$key] = $value; 39 | } 40 | } 41 | return $result; 42 | } 43 | 44 | function key_by_value(array $array) { 45 | $result = []; 46 | foreach ($array as $key => $value) { 47 | $result[$value] = $value; 48 | } 49 | return $result; 50 | } 51 | 52 | function array_concat_in_place(& $arr1, $arr2) { 53 | foreach ($arr2 as $key => $value) { 54 | $arr1[] = $value; 55 | } 56 | } 57 | 58 | function array_concat(array $arr1 /*array2, ...*/) { 59 | $args = func_get_args(); 60 | foreach ($args as $key => $arr2) { 61 | if ($key !== 0) { 62 | foreach ($arr2 as $key => $value) { 63 | $arr1[] = $value; 64 | } 65 | } 66 | } 67 | return $arr1; 68 | } 69 | 70 | function array_flatten(array $array) { 71 | $result = []; 72 | foreach ($array as $key => $value) { 73 | if (is_array($value)) { 74 | array_concat_in_place($result, array_flatten($value)); 75 | } else { 76 | $result[] = $value; 77 | } 78 | } 79 | return $result; 80 | } 81 | 82 | function make_array($val) { 83 | return is_array($val) ? $val : [$val]; 84 | } 85 | 86 | function array_select_keysx(array $dict, array $keys, $custom_error = null) { 87 | $result = []; 88 | foreach ($keys as $key) { 89 | if (array_key_exists($key, $dict)) { 90 | $result[$key] = $dict[$key]; 91 | } else { 92 | if (is_callable($custom_error)) { 93 | $custom_error(); 94 | } else { 95 | throw new GPException('Missing key '.$key.' in '.json_encode($dict), 1); 96 | } 97 | } 98 | } 99 | return $result; 100 | } 101 | 102 | function array_unset_keys(array & $array, array $keys) { 103 | foreach ($keys as $key) { 104 | unset($array[$key]); 105 | } 106 | } 107 | 108 | function array_append(array $array, $elem, $key = null) { 109 | if ($key === null) { 110 | $array[] = $elem; 111 | } else { 112 | $array[$key] = $elem; 113 | } 114 | return $array; 115 | } 116 | 117 | function array_key_by_value(array $array) { 118 | $new_array = []; 119 | foreach ($array as $key => $value) { 120 | $new_array[$value] = $value; 121 | } 122 | return $new_array; 123 | } 124 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | getMessage(), 15 | $e->getTraceAsString(), 16 | ]; 17 | 18 | if (GPEnv::isDevEnv()) { 19 | echo str_replace("\n", '
', implode('
', $error)); 20 | throw $e; 21 | } 22 | error_log(implode("\n", $error)); 23 | } 24 | -------------------------------------------------------------------------------- /sample_app/controllers/Posts.php: -------------------------------------------------------------------------------- 1 | Post::getAll()]); 13 | } 14 | 15 | public function one($id) { 16 | $post = Post::getByID($id); 17 | if (!$post) { 18 | GP::return404(); 19 | } 20 | $post->loadComments(); 21 | GP::view('one_post', ['post' => $post]); 22 | } 23 | 24 | public function create() { 25 | $text = $this->post->getString('text'); 26 | $post = (new Post())->setText($text)->save(); 27 | $post->addCreator(User::getByID(GPSession::get('user_id')))->save(); 28 | Posts::redirect(); 29 | } 30 | 31 | public function createComment() { 32 | $text = $this->post->getString('text'); 33 | $post_id = $this->post->getString('post_id'); 34 | $post = Post::getByID($post_id); 35 | $comment = (new Comment())->setText($text)->save(); 36 | $comment->addCreator(User::getByID(GPSession::get('user_id')))->save(); 37 | $post->addComments($comment)->save(); 38 | Posts::redirect()->one($post_id); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /sample_app/controllers/Users.php: -------------------------------------------------------------------------------- 1 | post->getString('email'); 7 | $password = $this->post->getString('password'); 8 | $user = User::getOneByEmail($email); 9 | if (!$user || !password_verify($password, $user->getPassword())) { 10 | Welcome::redirect(); 11 | } 12 | GPSession::set('user_id', $user->getID()); 13 | Posts::redirect(); 14 | } 15 | 16 | public function create() { 17 | $email = $this->post->getString('email'); 18 | $password = $this->post->getString('password'); 19 | $user = User::getOneByEmail($email); 20 | if ($user) { 21 | Welcome::redirect(); 22 | } 23 | $user = (new User()) 24 | ->setEmail($email) 25 | ->setPassword(password_hash($password, PASSWORD_DEFAULT)) 26 | ->save(); 27 | $this->login(); 28 | } 29 | 30 | public function logout() { 31 | GPSession::destroy(); 32 | Welcome::redirect(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sample_app/controllers/Welcome.php: -------------------------------------------------------------------------------- 1 | admin_enabled) { 7 | GP::return404(); 8 | } 9 | } 10 | 11 | public function index() { 12 | $data = [ 13 | 'types' => GPNodeMap::regenAndGetAllTypes(), 14 | 'counts' => ipull(GPDatabase::get()->getTypeCounts(), 'count', 'type'), 15 | ]; 16 | GPDatabase::get()->dispose(); 17 | GP::viewWithLayout('admin/explore_view', 'layout/admin_layout', $data); 18 | } 19 | 20 | public function node_type($type) { 21 | $name = GPNodeMap::getClass($type); 22 | $data = [ 23 | 'type' => $type, 24 | 'name' => $name, 25 | 'nodes' => $name::getAll(), 26 | ]; 27 | GPDatabase::get()->dispose(); 28 | GP::viewWithLayout('admin/node_type_view', 'layout/admin_layout', $data); 29 | } 30 | 31 | public function node($id) { 32 | $node = GPNode::getByID($id); 33 | if (!$node) { 34 | self::redirect(); 35 | } 36 | $node->loadConnectedNodes($node::getEdgeTypes()); 37 | GP::viewWithLayout( 38 | 'admin/node_view', 39 | 'layout/admin_layout', 40 | ['node' => $node] 41 | ); 42 | } 43 | 44 | public function edges() { 45 | $node_classes = GPNodeMap::regenAndGetAllTypes(); 46 | $edges = []; 47 | foreach ($node_classes as $class) { 48 | array_concat_in_place($edges, $class::getEdgeTypes()); 49 | } 50 | GP::viewWithLayout( 51 | 'admin/edge_view', 52 | 'layout/admin_layout', 53 | ['edges' => $edges] 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /sample_app/controllers/admin/AdminAjax.php: -------------------------------------------------------------------------------- 1 | admin_enabled) { 7 | GP::return404(); 8 | } 9 | } 10 | 11 | public function create($type) { 12 | if ($this->post->getExists('create')) { 13 | GPNode::createFromType($type)->save(); 14 | } 15 | Admin::redirect()->node_type($type); 16 | } 17 | 18 | public function delete($type) { 19 | if ($this->post->getInt('delete_node_id')) { 20 | GPNode::getByID($this->post->getInt('delete_node_id'))->delete(); 21 | } 22 | Admin::redirect()->node_type($type); 23 | } 24 | 25 | public function save($id) { 26 | $node = GPNode::getByID($id); 27 | $key = $this->post->getString('data_key'); 28 | $val = $this->post->getString('data_val'); 29 | $key_to_unset = $this->post->getString('data_key_to_unset'); 30 | if ($key && $val) { 31 | $data_type = $node::getDataTypeByName($key); 32 | if ( 33 | $data_type !== null && 34 | $data_type->getType() === GPDataType::GP_ARRAY 35 | ) { 36 | $val = json_decode($val, true); 37 | } else if ( 38 | $data_type !== null && 39 | $data_type->getType() === GPDataType::GP_BOOL 40 | ) { 41 | $val = (bool) $val; 42 | } 43 | $node->setData($key, $val)->save(); 44 | } 45 | if ($key_to_unset) { 46 | $node->unsetData($key_to_unset)->save(); 47 | } 48 | $edge_type = $this->post->get('edge_type'); 49 | if ($edge_type && $this->post->getInt('to_id')) { 50 | $edge = is_numeric($edge_type) ? 51 | $node::getEdgeTypeByType($edge_type) : 52 | $node::getEdgeType($edge_type); 53 | $other_node = GPNode::getByID($this->post->getInt('to_id')); 54 | if ($this->post->getExists('delete')) { 55 | $node->addPendingRemovalNodes($edge, [$other_node]); 56 | } else { 57 | $node->addPendingConnectedNodes($edge, [$other_node]); 58 | } 59 | $node->save(); 60 | } 61 | Admin::redirect()->node($id); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /sample_app/libraries/StringLibrary.php: -------------------------------------------------------------------------------- 1 | $length) { 7 | $string = mb_substr($string, 0, $length - 3).'...'; 8 | } 9 | return $string; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /sample_app/models/Comment.php: -------------------------------------------------------------------------------- 1 | setSingleNodeName('post'), 14 | (new GPEdgeType(User::class, 'creators'))->setSingleNodeName('creator'), 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sample_app/models/Post.php: -------------------------------------------------------------------------------- 1 | inverse(Comment::getEdgeType('post')), 15 | (new GPEdgeType(User::class, 'creators'))->setSingleNodeName('creator'), 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /sample_app/models/User.php: -------------------------------------------------------------------------------- 1 | inverse(Comment::getEdgeType('creator')), 16 | (new GPEdgeType(Comment::class, 'comments')) 17 | ->inverse(Comment::getEdgeType('creator')), 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sample_app/views/admin/edge_view.php: -------------------------------------------------------------------------------- 1 |

Edges

2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
TypeNameFrom TypeTo Type
getType()?>getName()?>getFromType())?>getToType())?>
23 |
-------------------------------------------------------------------------------- /sample_app/views/admin/explore_view.php: -------------------------------------------------------------------------------- 1 |

Node Types

2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | $name): ?> 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
TypeNameCount
21 |
22 | -------------------------------------------------------------------------------- /sample_app/views/admin/node_type_view.php: -------------------------------------------------------------------------------- 1 |

2 |
3 | 4 | 5 | 8 | 9 |
10 |

11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 40 | 51 | 52 | 53 | 54 |
IDDataUpdatedEditDelete
27 | 28 | getID()?> 29 | 30 | getJSONData(), 0, 128)?>getUpdated()?> 34 | 37 | Edit 38 | 39 | 41 |
42 | 43 | 48 | 49 |
50 |
55 |
56 | -------------------------------------------------------------------------------- /sample_app/views/admin/node_view.php: -------------------------------------------------------------------------------- 1 | 2 | back to list 3 | 4 |

ID getID() ?>

5 |

Data:

6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | getDataArray() as $key => $value): ?> 18 | 19 | 20 | 21 | 24 | 36 | 37 | 38 | 39 |
KeyValueIndexedUnset
22 | getIndexedData(), $key) ? 'yes' : 'no'?> 23 | 25 |
29 | 30 | 31 | 34 |
35 |
40 |
41 |
42 | 43 | 44 | 45 | 48 |
49 | 50 |

Edges:

51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | getConnectedNodes($node::getEdgeTypes()) 62 | as $e => $nodes): ?> 63 | 64 | 65 | 68 | 76 | 93 | 94 | 95 | 96 | 97 |
EdgeTo NodeDelete
66 | getName().' - '.$e?> 67 | 69 | 70 | Link 71 | 72 | ID: getID()?> 73 | type: 74 | 75 | 77 |
81 | 82 | 83 | 87 | 88 | 91 |
92 |
98 |
99 |
100 | 101 | 102 | 103 | 106 |
107 | -------------------------------------------------------------------------------- /sample_app/views/layout/admin_layout.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Node Explorer 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 33 |
34 | 37 | 38 |
39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /sample_app/views/login_view.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
-------------------------------------------------------------------------------- /sample_app/views/one_post.php: -------------------------------------------------------------------------------- 1 |

2 | getText()?> 3 |

4 | 5 | 10 |
11 | 12 | 13 | 14 | 15 | 16 |
-------------------------------------------------------------------------------- /sample_app/views/post_list.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
7 | -------------------------------------------------------------------------------- /third_party/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require-dev": { 3 | "phpunit/phpunit": "^8.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /third_party/composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "b66857c88c2926078b83550fb62c48e9", 8 | "packages": [], 9 | "packages-dev": [ 10 | { 11 | "name": "doctrine/instantiator", 12 | "version": "1.1.0", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/doctrine/instantiator.git", 16 | "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", 21 | "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "php": "^7.1" 26 | }, 27 | "require-dev": { 28 | "athletic/athletic": "~0.1.8", 29 | "ext-pdo": "*", 30 | "ext-phar": "*", 31 | "phpunit/phpunit": "^6.2.3", 32 | "squizlabs/php_codesniffer": "^3.0.2" 33 | }, 34 | "type": "library", 35 | "extra": { 36 | "branch-alias": { 37 | "dev-master": "1.2.x-dev" 38 | } 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 43 | } 44 | }, 45 | "notification-url": "https://packagist.org/downloads/", 46 | "license": [ 47 | "MIT" 48 | ], 49 | "authors": [ 50 | { 51 | "name": "Marco Pivetta", 52 | "email": "ocramius@gmail.com", 53 | "homepage": "http://ocramius.github.com/" 54 | } 55 | ], 56 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 57 | "homepage": "https://github.com/doctrine/instantiator", 58 | "keywords": [ 59 | "constructor", 60 | "instantiate" 61 | ], 62 | "time": "2017-07-22T11:58:36+00:00" 63 | }, 64 | { 65 | "name": "myclabs/deep-copy", 66 | "version": "1.8.1", 67 | "source": { 68 | "type": "git", 69 | "url": "https://github.com/myclabs/DeepCopy.git", 70 | "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8" 71 | }, 72 | "dist": { 73 | "type": "zip", 74 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8", 75 | "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8", 76 | "shasum": "" 77 | }, 78 | "require": { 79 | "php": "^7.1" 80 | }, 81 | "replace": { 82 | "myclabs/deep-copy": "self.version" 83 | }, 84 | "require-dev": { 85 | "doctrine/collections": "^1.0", 86 | "doctrine/common": "^2.6", 87 | "phpunit/phpunit": "^7.1" 88 | }, 89 | "type": "library", 90 | "autoload": { 91 | "psr-4": { 92 | "DeepCopy\\": "src/DeepCopy/" 93 | }, 94 | "files": [ 95 | "src/DeepCopy/deep_copy.php" 96 | ] 97 | }, 98 | "notification-url": "https://packagist.org/downloads/", 99 | "license": [ 100 | "MIT" 101 | ], 102 | "description": "Create deep copies (clones) of your objects", 103 | "keywords": [ 104 | "clone", 105 | "copy", 106 | "duplicate", 107 | "object", 108 | "object graph" 109 | ], 110 | "time": "2018-06-11T23:09:50+00:00" 111 | }, 112 | { 113 | "name": "phar-io/manifest", 114 | "version": "1.0.3", 115 | "source": { 116 | "type": "git", 117 | "url": "https://github.com/phar-io/manifest.git", 118 | "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" 119 | }, 120 | "dist": { 121 | "type": "zip", 122 | "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", 123 | "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", 124 | "shasum": "" 125 | }, 126 | "require": { 127 | "ext-dom": "*", 128 | "ext-phar": "*", 129 | "phar-io/version": "^2.0", 130 | "php": "^5.6 || ^7.0" 131 | }, 132 | "type": "library", 133 | "extra": { 134 | "branch-alias": { 135 | "dev-master": "1.0.x-dev" 136 | } 137 | }, 138 | "autoload": { 139 | "classmap": [ 140 | "src/" 141 | ] 142 | }, 143 | "notification-url": "https://packagist.org/downloads/", 144 | "license": [ 145 | "BSD-3-Clause" 146 | ], 147 | "authors": [ 148 | { 149 | "name": "Arne Blankerts", 150 | "email": "arne@blankerts.de", 151 | "role": "Developer" 152 | }, 153 | { 154 | "name": "Sebastian Heuer", 155 | "email": "sebastian@phpeople.de", 156 | "role": "Developer" 157 | }, 158 | { 159 | "name": "Sebastian Bergmann", 160 | "email": "sebastian@phpunit.de", 161 | "role": "Developer" 162 | } 163 | ], 164 | "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", 165 | "time": "2018-07-08T19:23:20+00:00" 166 | }, 167 | { 168 | "name": "phar-io/version", 169 | "version": "2.0.1", 170 | "source": { 171 | "type": "git", 172 | "url": "https://github.com/phar-io/version.git", 173 | "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" 174 | }, 175 | "dist": { 176 | "type": "zip", 177 | "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", 178 | "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", 179 | "shasum": "" 180 | }, 181 | "require": { 182 | "php": "^5.6 || ^7.0" 183 | }, 184 | "type": "library", 185 | "autoload": { 186 | "classmap": [ 187 | "src/" 188 | ] 189 | }, 190 | "notification-url": "https://packagist.org/downloads/", 191 | "license": [ 192 | "BSD-3-Clause" 193 | ], 194 | "authors": [ 195 | { 196 | "name": "Arne Blankerts", 197 | "email": "arne@blankerts.de", 198 | "role": "Developer" 199 | }, 200 | { 201 | "name": "Sebastian Heuer", 202 | "email": "sebastian@phpeople.de", 203 | "role": "Developer" 204 | }, 205 | { 206 | "name": "Sebastian Bergmann", 207 | "email": "sebastian@phpunit.de", 208 | "role": "Developer" 209 | } 210 | ], 211 | "description": "Library for handling version information and constraints", 212 | "time": "2018-07-08T19:19:57+00:00" 213 | }, 214 | { 215 | "name": "phpdocumentor/reflection-common", 216 | "version": "1.0.1", 217 | "source": { 218 | "type": "git", 219 | "url": "https://github.com/phpDocumentor/ReflectionCommon.git", 220 | "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" 221 | }, 222 | "dist": { 223 | "type": "zip", 224 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", 225 | "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", 226 | "shasum": "" 227 | }, 228 | "require": { 229 | "php": ">=5.5" 230 | }, 231 | "require-dev": { 232 | "phpunit/phpunit": "^4.6" 233 | }, 234 | "type": "library", 235 | "extra": { 236 | "branch-alias": { 237 | "dev-master": "1.0.x-dev" 238 | } 239 | }, 240 | "autoload": { 241 | "psr-4": { 242 | "phpDocumentor\\Reflection\\": [ 243 | "src" 244 | ] 245 | } 246 | }, 247 | "notification-url": "https://packagist.org/downloads/", 248 | "license": [ 249 | "MIT" 250 | ], 251 | "authors": [ 252 | { 253 | "name": "Jaap van Otterdijk", 254 | "email": "opensource@ijaap.nl" 255 | } 256 | ], 257 | "description": "Common reflection classes used by phpdocumentor to reflect the code structure", 258 | "homepage": "http://www.phpdoc.org", 259 | "keywords": [ 260 | "FQSEN", 261 | "phpDocumentor", 262 | "phpdoc", 263 | "reflection", 264 | "static analysis" 265 | ], 266 | "time": "2017-09-11T18:02:19+00:00" 267 | }, 268 | { 269 | "name": "phpdocumentor/reflection-docblock", 270 | "version": "4.3.0", 271 | "source": { 272 | "type": "git", 273 | "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 274 | "reference": "94fd0001232e47129dd3504189fa1c7225010d08" 275 | }, 276 | "dist": { 277 | "type": "zip", 278 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08", 279 | "reference": "94fd0001232e47129dd3504189fa1c7225010d08", 280 | "shasum": "" 281 | }, 282 | "require": { 283 | "php": "^7.0", 284 | "phpdocumentor/reflection-common": "^1.0.0", 285 | "phpdocumentor/type-resolver": "^0.4.0", 286 | "webmozart/assert": "^1.0" 287 | }, 288 | "require-dev": { 289 | "doctrine/instantiator": "~1.0.5", 290 | "mockery/mockery": "^1.0", 291 | "phpunit/phpunit": "^6.4" 292 | }, 293 | "type": "library", 294 | "extra": { 295 | "branch-alias": { 296 | "dev-master": "4.x-dev" 297 | } 298 | }, 299 | "autoload": { 300 | "psr-4": { 301 | "phpDocumentor\\Reflection\\": [ 302 | "src/" 303 | ] 304 | } 305 | }, 306 | "notification-url": "https://packagist.org/downloads/", 307 | "license": [ 308 | "MIT" 309 | ], 310 | "authors": [ 311 | { 312 | "name": "Mike van Riel", 313 | "email": "me@mikevanriel.com" 314 | } 315 | ], 316 | "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", 317 | "time": "2017-11-30T07:14:17+00:00" 318 | }, 319 | { 320 | "name": "phpdocumentor/type-resolver", 321 | "version": "0.4.0", 322 | "source": { 323 | "type": "git", 324 | "url": "https://github.com/phpDocumentor/TypeResolver.git", 325 | "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" 326 | }, 327 | "dist": { 328 | "type": "zip", 329 | "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", 330 | "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", 331 | "shasum": "" 332 | }, 333 | "require": { 334 | "php": "^5.5 || ^7.0", 335 | "phpdocumentor/reflection-common": "^1.0" 336 | }, 337 | "require-dev": { 338 | "mockery/mockery": "^0.9.4", 339 | "phpunit/phpunit": "^5.2||^4.8.24" 340 | }, 341 | "type": "library", 342 | "extra": { 343 | "branch-alias": { 344 | "dev-master": "1.0.x-dev" 345 | } 346 | }, 347 | "autoload": { 348 | "psr-4": { 349 | "phpDocumentor\\Reflection\\": [ 350 | "src/" 351 | ] 352 | } 353 | }, 354 | "notification-url": "https://packagist.org/downloads/", 355 | "license": [ 356 | "MIT" 357 | ], 358 | "authors": [ 359 | { 360 | "name": "Mike van Riel", 361 | "email": "me@mikevanriel.com" 362 | } 363 | ], 364 | "time": "2017-07-14T14:27:02+00:00" 365 | }, 366 | { 367 | "name": "phpspec/prophecy", 368 | "version": "1.8.0", 369 | "source": { 370 | "type": "git", 371 | "url": "https://github.com/phpspec/prophecy.git", 372 | "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06" 373 | }, 374 | "dist": { 375 | "type": "zip", 376 | "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06", 377 | "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06", 378 | "shasum": "" 379 | }, 380 | "require": { 381 | "doctrine/instantiator": "^1.0.2", 382 | "php": "^5.3|^7.0", 383 | "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", 384 | "sebastian/comparator": "^1.1|^2.0|^3.0", 385 | "sebastian/recursion-context": "^1.0|^2.0|^3.0" 386 | }, 387 | "require-dev": { 388 | "phpspec/phpspec": "^2.5|^3.2", 389 | "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" 390 | }, 391 | "type": "library", 392 | "extra": { 393 | "branch-alias": { 394 | "dev-master": "1.8.x-dev" 395 | } 396 | }, 397 | "autoload": { 398 | "psr-0": { 399 | "Prophecy\\": "src/" 400 | } 401 | }, 402 | "notification-url": "https://packagist.org/downloads/", 403 | "license": [ 404 | "MIT" 405 | ], 406 | "authors": [ 407 | { 408 | "name": "Konstantin Kudryashov", 409 | "email": "ever.zet@gmail.com", 410 | "homepage": "http://everzet.com" 411 | }, 412 | { 413 | "name": "Marcello Duarte", 414 | "email": "marcello.duarte@gmail.com" 415 | } 416 | ], 417 | "description": "Highly opinionated mocking framework for PHP 5.3+", 418 | "homepage": "https://github.com/phpspec/prophecy", 419 | "keywords": [ 420 | "Double", 421 | "Dummy", 422 | "fake", 423 | "mock", 424 | "spy", 425 | "stub" 426 | ], 427 | "time": "2018-08-05T17:53:17+00:00" 428 | }, 429 | { 430 | "name": "phpunit/php-code-coverage", 431 | "version": "7.0.1", 432 | "source": { 433 | "type": "git", 434 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 435 | "reference": "4832739a02c418397e404da6c3e4fe680b7a4de7" 436 | }, 437 | "dist": { 438 | "type": "zip", 439 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4832739a02c418397e404da6c3e4fe680b7a4de7", 440 | "reference": "4832739a02c418397e404da6c3e4fe680b7a4de7", 441 | "shasum": "" 442 | }, 443 | "require": { 444 | "ext-dom": "*", 445 | "ext-xmlwriter": "*", 446 | "php": "^7.2", 447 | "phpunit/php-file-iterator": "^2.0.2", 448 | "phpunit/php-text-template": "^1.2.1", 449 | "phpunit/php-token-stream": "^3.0.1", 450 | "sebastian/code-unit-reverse-lookup": "^1.0.1", 451 | "sebastian/environment": "^4.1", 452 | "sebastian/version": "^2.0.1", 453 | "theseer/tokenizer": "^1.1" 454 | }, 455 | "require-dev": { 456 | "phpunit/phpunit": "^8.0" 457 | }, 458 | "suggest": { 459 | "ext-xdebug": "^2.6.1" 460 | }, 461 | "type": "library", 462 | "extra": { 463 | "branch-alias": { 464 | "dev-master": "7.0-dev" 465 | } 466 | }, 467 | "autoload": { 468 | "classmap": [ 469 | "src/" 470 | ] 471 | }, 472 | "notification-url": "https://packagist.org/downloads/", 473 | "license": [ 474 | "BSD-3-Clause" 475 | ], 476 | "authors": [ 477 | { 478 | "name": "Sebastian Bergmann", 479 | "email": "sebastian@phpunit.de", 480 | "role": "lead" 481 | } 482 | ], 483 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 484 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 485 | "keywords": [ 486 | "coverage", 487 | "testing", 488 | "xunit" 489 | ], 490 | "time": "2019-02-01T07:29:14+00:00" 491 | }, 492 | { 493 | "name": "phpunit/php-file-iterator", 494 | "version": "2.0.2", 495 | "source": { 496 | "type": "git", 497 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 498 | "reference": "050bedf145a257b1ff02746c31894800e5122946" 499 | }, 500 | "dist": { 501 | "type": "zip", 502 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946", 503 | "reference": "050bedf145a257b1ff02746c31894800e5122946", 504 | "shasum": "" 505 | }, 506 | "require": { 507 | "php": "^7.1" 508 | }, 509 | "require-dev": { 510 | "phpunit/phpunit": "^7.1" 511 | }, 512 | "type": "library", 513 | "extra": { 514 | "branch-alias": { 515 | "dev-master": "2.0.x-dev" 516 | } 517 | }, 518 | "autoload": { 519 | "classmap": [ 520 | "src/" 521 | ] 522 | }, 523 | "notification-url": "https://packagist.org/downloads/", 524 | "license": [ 525 | "BSD-3-Clause" 526 | ], 527 | "authors": [ 528 | { 529 | "name": "Sebastian Bergmann", 530 | "email": "sebastian@phpunit.de", 531 | "role": "lead" 532 | } 533 | ], 534 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 535 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 536 | "keywords": [ 537 | "filesystem", 538 | "iterator" 539 | ], 540 | "time": "2018-09-13T20:33:42+00:00" 541 | }, 542 | { 543 | "name": "phpunit/php-text-template", 544 | "version": "1.2.1", 545 | "source": { 546 | "type": "git", 547 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 548 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" 549 | }, 550 | "dist": { 551 | "type": "zip", 552 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 553 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 554 | "shasum": "" 555 | }, 556 | "require": { 557 | "php": ">=5.3.3" 558 | }, 559 | "type": "library", 560 | "autoload": { 561 | "classmap": [ 562 | "src/" 563 | ] 564 | }, 565 | "notification-url": "https://packagist.org/downloads/", 566 | "license": [ 567 | "BSD-3-Clause" 568 | ], 569 | "authors": [ 570 | { 571 | "name": "Sebastian Bergmann", 572 | "email": "sebastian@phpunit.de", 573 | "role": "lead" 574 | } 575 | ], 576 | "description": "Simple template engine.", 577 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 578 | "keywords": [ 579 | "template" 580 | ], 581 | "time": "2015-06-21T13:50:34+00:00" 582 | }, 583 | { 584 | "name": "phpunit/php-timer", 585 | "version": "2.0.0", 586 | "source": { 587 | "type": "git", 588 | "url": "https://github.com/sebastianbergmann/php-timer.git", 589 | "reference": "8b8454ea6958c3dee38453d3bd571e023108c91f" 590 | }, 591 | "dist": { 592 | "type": "zip", 593 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/8b8454ea6958c3dee38453d3bd571e023108c91f", 594 | "reference": "8b8454ea6958c3dee38453d3bd571e023108c91f", 595 | "shasum": "" 596 | }, 597 | "require": { 598 | "php": "^7.1" 599 | }, 600 | "require-dev": { 601 | "phpunit/phpunit": "^7.0" 602 | }, 603 | "type": "library", 604 | "extra": { 605 | "branch-alias": { 606 | "dev-master": "2.0-dev" 607 | } 608 | }, 609 | "autoload": { 610 | "classmap": [ 611 | "src/" 612 | ] 613 | }, 614 | "notification-url": "https://packagist.org/downloads/", 615 | "license": [ 616 | "BSD-3-Clause" 617 | ], 618 | "authors": [ 619 | { 620 | "name": "Sebastian Bergmann", 621 | "email": "sebastian@phpunit.de", 622 | "role": "lead" 623 | } 624 | ], 625 | "description": "Utility class for timing", 626 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 627 | "keywords": [ 628 | "timer" 629 | ], 630 | "time": "2018-02-01T13:07:23+00:00" 631 | }, 632 | { 633 | "name": "phpunit/php-token-stream", 634 | "version": "3.0.1", 635 | "source": { 636 | "type": "git", 637 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 638 | "reference": "c99e3be9d3e85f60646f152f9002d46ed7770d18" 639 | }, 640 | "dist": { 641 | "type": "zip", 642 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/c99e3be9d3e85f60646f152f9002d46ed7770d18", 643 | "reference": "c99e3be9d3e85f60646f152f9002d46ed7770d18", 644 | "shasum": "" 645 | }, 646 | "require": { 647 | "ext-tokenizer": "*", 648 | "php": "^7.1" 649 | }, 650 | "require-dev": { 651 | "phpunit/phpunit": "^7.0" 652 | }, 653 | "type": "library", 654 | "extra": { 655 | "branch-alias": { 656 | "dev-master": "3.0-dev" 657 | } 658 | }, 659 | "autoload": { 660 | "classmap": [ 661 | "src/" 662 | ] 663 | }, 664 | "notification-url": "https://packagist.org/downloads/", 665 | "license": [ 666 | "BSD-3-Clause" 667 | ], 668 | "authors": [ 669 | { 670 | "name": "Sebastian Bergmann", 671 | "email": "sebastian@phpunit.de" 672 | } 673 | ], 674 | "description": "Wrapper around PHP's tokenizer extension.", 675 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 676 | "keywords": [ 677 | "tokenizer" 678 | ], 679 | "time": "2018-10-30T05:52:18+00:00" 680 | }, 681 | { 682 | "name": "phpunit/phpunit", 683 | "version": "8.0.4", 684 | "source": { 685 | "type": "git", 686 | "url": "https://github.com/sebastianbergmann/phpunit.git", 687 | "reference": "a7af0201285445c9c73c4bdf869c486e36b41604" 688 | }, 689 | "dist": { 690 | "type": "zip", 691 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a7af0201285445c9c73c4bdf869c486e36b41604", 692 | "reference": "a7af0201285445c9c73c4bdf869c486e36b41604", 693 | "shasum": "" 694 | }, 695 | "require": { 696 | "doctrine/instantiator": "^1.1", 697 | "ext-dom": "*", 698 | "ext-json": "*", 699 | "ext-libxml": "*", 700 | "ext-mbstring": "*", 701 | "ext-xml": "*", 702 | "ext-xmlwriter": "*", 703 | "myclabs/deep-copy": "^1.7", 704 | "phar-io/manifest": "^1.0.2", 705 | "phar-io/version": "^2.0", 706 | "php": "^7.2", 707 | "phpspec/prophecy": "^1.7", 708 | "phpunit/php-code-coverage": "^7.0", 709 | "phpunit/php-file-iterator": "^2.0.1", 710 | "phpunit/php-text-template": "^1.2.1", 711 | "phpunit/php-timer": "^2.0", 712 | "sebastian/comparator": "^3.0", 713 | "sebastian/diff": "^3.0", 714 | "sebastian/environment": "^4.1", 715 | "sebastian/exporter": "^3.1", 716 | "sebastian/global-state": "^3.0", 717 | "sebastian/object-enumerator": "^3.0.3", 718 | "sebastian/resource-operations": "^2.0", 719 | "sebastian/version": "^2.0.1" 720 | }, 721 | "require-dev": { 722 | "ext-pdo": "*" 723 | }, 724 | "suggest": { 725 | "ext-soap": "*", 726 | "ext-xdebug": "*", 727 | "phpunit/php-invoker": "^2.0" 728 | }, 729 | "bin": [ 730 | "phpunit" 731 | ], 732 | "type": "library", 733 | "extra": { 734 | "branch-alias": { 735 | "dev-master": "8.0-dev" 736 | } 737 | }, 738 | "autoload": { 739 | "classmap": [ 740 | "src/" 741 | ] 742 | }, 743 | "notification-url": "https://packagist.org/downloads/", 744 | "license": [ 745 | "BSD-3-Clause" 746 | ], 747 | "authors": [ 748 | { 749 | "name": "Sebastian Bergmann", 750 | "email": "sebastian@phpunit.de", 751 | "role": "lead" 752 | } 753 | ], 754 | "description": "The PHP Unit Testing framework.", 755 | "homepage": "https://phpunit.de/", 756 | "keywords": [ 757 | "phpunit", 758 | "testing", 759 | "xunit" 760 | ], 761 | "time": "2019-02-18T09:23:05+00:00" 762 | }, 763 | { 764 | "name": "sebastian/code-unit-reverse-lookup", 765 | "version": "1.0.1", 766 | "source": { 767 | "type": "git", 768 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", 769 | "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" 770 | }, 771 | "dist": { 772 | "type": "zip", 773 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", 774 | "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", 775 | "shasum": "" 776 | }, 777 | "require": { 778 | "php": "^5.6 || ^7.0" 779 | }, 780 | "require-dev": { 781 | "phpunit/phpunit": "^5.7 || ^6.0" 782 | }, 783 | "type": "library", 784 | "extra": { 785 | "branch-alias": { 786 | "dev-master": "1.0.x-dev" 787 | } 788 | }, 789 | "autoload": { 790 | "classmap": [ 791 | "src/" 792 | ] 793 | }, 794 | "notification-url": "https://packagist.org/downloads/", 795 | "license": [ 796 | "BSD-3-Clause" 797 | ], 798 | "authors": [ 799 | { 800 | "name": "Sebastian Bergmann", 801 | "email": "sebastian@phpunit.de" 802 | } 803 | ], 804 | "description": "Looks up which function or method a line of code belongs to", 805 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 806 | "time": "2017-03-04T06:30:41+00:00" 807 | }, 808 | { 809 | "name": "sebastian/comparator", 810 | "version": "3.0.2", 811 | "source": { 812 | "type": "git", 813 | "url": "https://github.com/sebastianbergmann/comparator.git", 814 | "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da" 815 | }, 816 | "dist": { 817 | "type": "zip", 818 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da", 819 | "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da", 820 | "shasum": "" 821 | }, 822 | "require": { 823 | "php": "^7.1", 824 | "sebastian/diff": "^3.0", 825 | "sebastian/exporter": "^3.1" 826 | }, 827 | "require-dev": { 828 | "phpunit/phpunit": "^7.1" 829 | }, 830 | "type": "library", 831 | "extra": { 832 | "branch-alias": { 833 | "dev-master": "3.0-dev" 834 | } 835 | }, 836 | "autoload": { 837 | "classmap": [ 838 | "src/" 839 | ] 840 | }, 841 | "notification-url": "https://packagist.org/downloads/", 842 | "license": [ 843 | "BSD-3-Clause" 844 | ], 845 | "authors": [ 846 | { 847 | "name": "Jeff Welch", 848 | "email": "whatthejeff@gmail.com" 849 | }, 850 | { 851 | "name": "Volker Dusch", 852 | "email": "github@wallbash.com" 853 | }, 854 | { 855 | "name": "Bernhard Schussek", 856 | "email": "bschussek@2bepublished.at" 857 | }, 858 | { 859 | "name": "Sebastian Bergmann", 860 | "email": "sebastian@phpunit.de" 861 | } 862 | ], 863 | "description": "Provides the functionality to compare PHP values for equality", 864 | "homepage": "https://github.com/sebastianbergmann/comparator", 865 | "keywords": [ 866 | "comparator", 867 | "compare", 868 | "equality" 869 | ], 870 | "time": "2018-07-12T15:12:46+00:00" 871 | }, 872 | { 873 | "name": "sebastian/diff", 874 | "version": "3.0.2", 875 | "source": { 876 | "type": "git", 877 | "url": "https://github.com/sebastianbergmann/diff.git", 878 | "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29" 879 | }, 880 | "dist": { 881 | "type": "zip", 882 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29", 883 | "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29", 884 | "shasum": "" 885 | }, 886 | "require": { 887 | "php": "^7.1" 888 | }, 889 | "require-dev": { 890 | "phpunit/phpunit": "^7.5 || ^8.0", 891 | "symfony/process": "^2 || ^3.3 || ^4" 892 | }, 893 | "type": "library", 894 | "extra": { 895 | "branch-alias": { 896 | "dev-master": "3.0-dev" 897 | } 898 | }, 899 | "autoload": { 900 | "classmap": [ 901 | "src/" 902 | ] 903 | }, 904 | "notification-url": "https://packagist.org/downloads/", 905 | "license": [ 906 | "BSD-3-Clause" 907 | ], 908 | "authors": [ 909 | { 910 | "name": "Kore Nordmann", 911 | "email": "mail@kore-nordmann.de" 912 | }, 913 | { 914 | "name": "Sebastian Bergmann", 915 | "email": "sebastian@phpunit.de" 916 | } 917 | ], 918 | "description": "Diff implementation", 919 | "homepage": "https://github.com/sebastianbergmann/diff", 920 | "keywords": [ 921 | "diff", 922 | "udiff", 923 | "unidiff", 924 | "unified diff" 925 | ], 926 | "time": "2019-02-04T06:01:07+00:00" 927 | }, 928 | { 929 | "name": "sebastian/environment", 930 | "version": "4.1.0", 931 | "source": { 932 | "type": "git", 933 | "url": "https://github.com/sebastianbergmann/environment.git", 934 | "reference": "6fda8ce1974b62b14935adc02a9ed38252eca656" 935 | }, 936 | "dist": { 937 | "type": "zip", 938 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6fda8ce1974b62b14935adc02a9ed38252eca656", 939 | "reference": "6fda8ce1974b62b14935adc02a9ed38252eca656", 940 | "shasum": "" 941 | }, 942 | "require": { 943 | "php": "^7.1" 944 | }, 945 | "require-dev": { 946 | "phpunit/phpunit": "^7.5" 947 | }, 948 | "suggest": { 949 | "ext-posix": "*" 950 | }, 951 | "type": "library", 952 | "extra": { 953 | "branch-alias": { 954 | "dev-master": "4.1-dev" 955 | } 956 | }, 957 | "autoload": { 958 | "classmap": [ 959 | "src/" 960 | ] 961 | }, 962 | "notification-url": "https://packagist.org/downloads/", 963 | "license": [ 964 | "BSD-3-Clause" 965 | ], 966 | "authors": [ 967 | { 968 | "name": "Sebastian Bergmann", 969 | "email": "sebastian@phpunit.de" 970 | } 971 | ], 972 | "description": "Provides functionality to handle HHVM/PHP environments", 973 | "homepage": "http://www.github.com/sebastianbergmann/environment", 974 | "keywords": [ 975 | "Xdebug", 976 | "environment", 977 | "hhvm" 978 | ], 979 | "time": "2019-02-01T05:27:49+00:00" 980 | }, 981 | { 982 | "name": "sebastian/exporter", 983 | "version": "3.1.0", 984 | "source": { 985 | "type": "git", 986 | "url": "https://github.com/sebastianbergmann/exporter.git", 987 | "reference": "234199f4528de6d12aaa58b612e98f7d36adb937" 988 | }, 989 | "dist": { 990 | "type": "zip", 991 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937", 992 | "reference": "234199f4528de6d12aaa58b612e98f7d36adb937", 993 | "shasum": "" 994 | }, 995 | "require": { 996 | "php": "^7.0", 997 | "sebastian/recursion-context": "^3.0" 998 | }, 999 | "require-dev": { 1000 | "ext-mbstring": "*", 1001 | "phpunit/phpunit": "^6.0" 1002 | }, 1003 | "type": "library", 1004 | "extra": { 1005 | "branch-alias": { 1006 | "dev-master": "3.1.x-dev" 1007 | } 1008 | }, 1009 | "autoload": { 1010 | "classmap": [ 1011 | "src/" 1012 | ] 1013 | }, 1014 | "notification-url": "https://packagist.org/downloads/", 1015 | "license": [ 1016 | "BSD-3-Clause" 1017 | ], 1018 | "authors": [ 1019 | { 1020 | "name": "Jeff Welch", 1021 | "email": "whatthejeff@gmail.com" 1022 | }, 1023 | { 1024 | "name": "Volker Dusch", 1025 | "email": "github@wallbash.com" 1026 | }, 1027 | { 1028 | "name": "Bernhard Schussek", 1029 | "email": "bschussek@2bepublished.at" 1030 | }, 1031 | { 1032 | "name": "Sebastian Bergmann", 1033 | "email": "sebastian@phpunit.de" 1034 | }, 1035 | { 1036 | "name": "Adam Harvey", 1037 | "email": "aharvey@php.net" 1038 | } 1039 | ], 1040 | "description": "Provides the functionality to export PHP variables for visualization", 1041 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 1042 | "keywords": [ 1043 | "export", 1044 | "exporter" 1045 | ], 1046 | "time": "2017-04-03T13:19:02+00:00" 1047 | }, 1048 | { 1049 | "name": "sebastian/global-state", 1050 | "version": "3.0.0", 1051 | "source": { 1052 | "type": "git", 1053 | "url": "https://github.com/sebastianbergmann/global-state.git", 1054 | "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4" 1055 | }, 1056 | "dist": { 1057 | "type": "zip", 1058 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", 1059 | "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", 1060 | "shasum": "" 1061 | }, 1062 | "require": { 1063 | "php": "^7.2", 1064 | "sebastian/object-reflector": "^1.1.1", 1065 | "sebastian/recursion-context": "^3.0" 1066 | }, 1067 | "require-dev": { 1068 | "ext-dom": "*", 1069 | "phpunit/phpunit": "^8.0" 1070 | }, 1071 | "suggest": { 1072 | "ext-uopz": "*" 1073 | }, 1074 | "type": "library", 1075 | "extra": { 1076 | "branch-alias": { 1077 | "dev-master": "3.0-dev" 1078 | } 1079 | }, 1080 | "autoload": { 1081 | "classmap": [ 1082 | "src/" 1083 | ] 1084 | }, 1085 | "notification-url": "https://packagist.org/downloads/", 1086 | "license": [ 1087 | "BSD-3-Clause" 1088 | ], 1089 | "authors": [ 1090 | { 1091 | "name": "Sebastian Bergmann", 1092 | "email": "sebastian@phpunit.de" 1093 | } 1094 | ], 1095 | "description": "Snapshotting of global state", 1096 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 1097 | "keywords": [ 1098 | "global state" 1099 | ], 1100 | "time": "2019-02-01T05:30:01+00:00" 1101 | }, 1102 | { 1103 | "name": "sebastian/object-enumerator", 1104 | "version": "3.0.3", 1105 | "source": { 1106 | "type": "git", 1107 | "url": "https://github.com/sebastianbergmann/object-enumerator.git", 1108 | "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" 1109 | }, 1110 | "dist": { 1111 | "type": "zip", 1112 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", 1113 | "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", 1114 | "shasum": "" 1115 | }, 1116 | "require": { 1117 | "php": "^7.0", 1118 | "sebastian/object-reflector": "^1.1.1", 1119 | "sebastian/recursion-context": "^3.0" 1120 | }, 1121 | "require-dev": { 1122 | "phpunit/phpunit": "^6.0" 1123 | }, 1124 | "type": "library", 1125 | "extra": { 1126 | "branch-alias": { 1127 | "dev-master": "3.0.x-dev" 1128 | } 1129 | }, 1130 | "autoload": { 1131 | "classmap": [ 1132 | "src/" 1133 | ] 1134 | }, 1135 | "notification-url": "https://packagist.org/downloads/", 1136 | "license": [ 1137 | "BSD-3-Clause" 1138 | ], 1139 | "authors": [ 1140 | { 1141 | "name": "Sebastian Bergmann", 1142 | "email": "sebastian@phpunit.de" 1143 | } 1144 | ], 1145 | "description": "Traverses array structures and object graphs to enumerate all referenced objects", 1146 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 1147 | "time": "2017-08-03T12:35:26+00:00" 1148 | }, 1149 | { 1150 | "name": "sebastian/object-reflector", 1151 | "version": "1.1.1", 1152 | "source": { 1153 | "type": "git", 1154 | "url": "https://github.com/sebastianbergmann/object-reflector.git", 1155 | "reference": "773f97c67f28de00d397be301821b06708fca0be" 1156 | }, 1157 | "dist": { 1158 | "type": "zip", 1159 | "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", 1160 | "reference": "773f97c67f28de00d397be301821b06708fca0be", 1161 | "shasum": "" 1162 | }, 1163 | "require": { 1164 | "php": "^7.0" 1165 | }, 1166 | "require-dev": { 1167 | "phpunit/phpunit": "^6.0" 1168 | }, 1169 | "type": "library", 1170 | "extra": { 1171 | "branch-alias": { 1172 | "dev-master": "1.1-dev" 1173 | } 1174 | }, 1175 | "autoload": { 1176 | "classmap": [ 1177 | "src/" 1178 | ] 1179 | }, 1180 | "notification-url": "https://packagist.org/downloads/", 1181 | "license": [ 1182 | "BSD-3-Clause" 1183 | ], 1184 | "authors": [ 1185 | { 1186 | "name": "Sebastian Bergmann", 1187 | "email": "sebastian@phpunit.de" 1188 | } 1189 | ], 1190 | "description": "Allows reflection of object attributes, including inherited and non-public ones", 1191 | "homepage": "https://github.com/sebastianbergmann/object-reflector/", 1192 | "time": "2017-03-29T09:07:27+00:00" 1193 | }, 1194 | { 1195 | "name": "sebastian/recursion-context", 1196 | "version": "3.0.0", 1197 | "source": { 1198 | "type": "git", 1199 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1200 | "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" 1201 | }, 1202 | "dist": { 1203 | "type": "zip", 1204 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", 1205 | "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", 1206 | "shasum": "" 1207 | }, 1208 | "require": { 1209 | "php": "^7.0" 1210 | }, 1211 | "require-dev": { 1212 | "phpunit/phpunit": "^6.0" 1213 | }, 1214 | "type": "library", 1215 | "extra": { 1216 | "branch-alias": { 1217 | "dev-master": "3.0.x-dev" 1218 | } 1219 | }, 1220 | "autoload": { 1221 | "classmap": [ 1222 | "src/" 1223 | ] 1224 | }, 1225 | "notification-url": "https://packagist.org/downloads/", 1226 | "license": [ 1227 | "BSD-3-Clause" 1228 | ], 1229 | "authors": [ 1230 | { 1231 | "name": "Jeff Welch", 1232 | "email": "whatthejeff@gmail.com" 1233 | }, 1234 | { 1235 | "name": "Sebastian Bergmann", 1236 | "email": "sebastian@phpunit.de" 1237 | }, 1238 | { 1239 | "name": "Adam Harvey", 1240 | "email": "aharvey@php.net" 1241 | } 1242 | ], 1243 | "description": "Provides functionality to recursively process PHP variables", 1244 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 1245 | "time": "2017-03-03T06:23:57+00:00" 1246 | }, 1247 | { 1248 | "name": "sebastian/resource-operations", 1249 | "version": "2.0.1", 1250 | "source": { 1251 | "type": "git", 1252 | "url": "https://github.com/sebastianbergmann/resource-operations.git", 1253 | "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9" 1254 | }, 1255 | "dist": { 1256 | "type": "zip", 1257 | "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9", 1258 | "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9", 1259 | "shasum": "" 1260 | }, 1261 | "require": { 1262 | "php": "^7.1" 1263 | }, 1264 | "type": "library", 1265 | "extra": { 1266 | "branch-alias": { 1267 | "dev-master": "2.0-dev" 1268 | } 1269 | }, 1270 | "autoload": { 1271 | "classmap": [ 1272 | "src/" 1273 | ] 1274 | }, 1275 | "notification-url": "https://packagist.org/downloads/", 1276 | "license": [ 1277 | "BSD-3-Clause" 1278 | ], 1279 | "authors": [ 1280 | { 1281 | "name": "Sebastian Bergmann", 1282 | "email": "sebastian@phpunit.de" 1283 | } 1284 | ], 1285 | "description": "Provides a list of PHP built-in functions that operate on resources", 1286 | "homepage": "https://www.github.com/sebastianbergmann/resource-operations", 1287 | "time": "2018-10-04T04:07:39+00:00" 1288 | }, 1289 | { 1290 | "name": "sebastian/version", 1291 | "version": "2.0.1", 1292 | "source": { 1293 | "type": "git", 1294 | "url": "https://github.com/sebastianbergmann/version.git", 1295 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" 1296 | }, 1297 | "dist": { 1298 | "type": "zip", 1299 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", 1300 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", 1301 | "shasum": "" 1302 | }, 1303 | "require": { 1304 | "php": ">=5.6" 1305 | }, 1306 | "type": "library", 1307 | "extra": { 1308 | "branch-alias": { 1309 | "dev-master": "2.0.x-dev" 1310 | } 1311 | }, 1312 | "autoload": { 1313 | "classmap": [ 1314 | "src/" 1315 | ] 1316 | }, 1317 | "notification-url": "https://packagist.org/downloads/", 1318 | "license": [ 1319 | "BSD-3-Clause" 1320 | ], 1321 | "authors": [ 1322 | { 1323 | "name": "Sebastian Bergmann", 1324 | "email": "sebastian@phpunit.de", 1325 | "role": "lead" 1326 | } 1327 | ], 1328 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 1329 | "homepage": "https://github.com/sebastianbergmann/version", 1330 | "time": "2016-10-03T07:35:21+00:00" 1331 | }, 1332 | { 1333 | "name": "symfony/polyfill-ctype", 1334 | "version": "v1.10.0", 1335 | "source": { 1336 | "type": "git", 1337 | "url": "https://github.com/symfony/polyfill-ctype.git", 1338 | "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" 1339 | }, 1340 | "dist": { 1341 | "type": "zip", 1342 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", 1343 | "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", 1344 | "shasum": "" 1345 | }, 1346 | "require": { 1347 | "php": ">=5.3.3" 1348 | }, 1349 | "suggest": { 1350 | "ext-ctype": "For best performance" 1351 | }, 1352 | "type": "library", 1353 | "extra": { 1354 | "branch-alias": { 1355 | "dev-master": "1.9-dev" 1356 | } 1357 | }, 1358 | "autoload": { 1359 | "psr-4": { 1360 | "Symfony\\Polyfill\\Ctype\\": "" 1361 | }, 1362 | "files": [ 1363 | "bootstrap.php" 1364 | ] 1365 | }, 1366 | "notification-url": "https://packagist.org/downloads/", 1367 | "license": [ 1368 | "MIT" 1369 | ], 1370 | "authors": [ 1371 | { 1372 | "name": "Symfony Community", 1373 | "homepage": "https://symfony.com/contributors" 1374 | }, 1375 | { 1376 | "name": "Gert de Pagter", 1377 | "email": "backendtea@gmail.com" 1378 | } 1379 | ], 1380 | "description": "Symfony polyfill for ctype functions", 1381 | "homepage": "https://symfony.com", 1382 | "keywords": [ 1383 | "compatibility", 1384 | "ctype", 1385 | "polyfill", 1386 | "portable" 1387 | ], 1388 | "time": "2018-08-06T14:22:27+00:00" 1389 | }, 1390 | { 1391 | "name": "theseer/tokenizer", 1392 | "version": "1.1.0", 1393 | "source": { 1394 | "type": "git", 1395 | "url": "https://github.com/theseer/tokenizer.git", 1396 | "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b" 1397 | }, 1398 | "dist": { 1399 | "type": "zip", 1400 | "url": "https://api.github.com/repos/theseer/tokenizer/zipball/cb2f008f3f05af2893a87208fe6a6c4985483f8b", 1401 | "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b", 1402 | "shasum": "" 1403 | }, 1404 | "require": { 1405 | "ext-dom": "*", 1406 | "ext-tokenizer": "*", 1407 | "ext-xmlwriter": "*", 1408 | "php": "^7.0" 1409 | }, 1410 | "type": "library", 1411 | "autoload": { 1412 | "classmap": [ 1413 | "src/" 1414 | ] 1415 | }, 1416 | "notification-url": "https://packagist.org/downloads/", 1417 | "license": [ 1418 | "BSD-3-Clause" 1419 | ], 1420 | "authors": [ 1421 | { 1422 | "name": "Arne Blankerts", 1423 | "email": "arne@blankerts.de", 1424 | "role": "Developer" 1425 | } 1426 | ], 1427 | "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", 1428 | "time": "2017-04-07T12:08:54+00:00" 1429 | }, 1430 | { 1431 | "name": "webmozart/assert", 1432 | "version": "1.4.0", 1433 | "source": { 1434 | "type": "git", 1435 | "url": "https://github.com/webmozart/assert.git", 1436 | "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9" 1437 | }, 1438 | "dist": { 1439 | "type": "zip", 1440 | "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9", 1441 | "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9", 1442 | "shasum": "" 1443 | }, 1444 | "require": { 1445 | "php": "^5.3.3 || ^7.0", 1446 | "symfony/polyfill-ctype": "^1.8" 1447 | }, 1448 | "require-dev": { 1449 | "phpunit/phpunit": "^4.6", 1450 | "sebastian/version": "^1.0.1" 1451 | }, 1452 | "type": "library", 1453 | "extra": { 1454 | "branch-alias": { 1455 | "dev-master": "1.3-dev" 1456 | } 1457 | }, 1458 | "autoload": { 1459 | "psr-4": { 1460 | "Webmozart\\Assert\\": "src/" 1461 | } 1462 | }, 1463 | "notification-url": "https://packagist.org/downloads/", 1464 | "license": [ 1465 | "MIT" 1466 | ], 1467 | "authors": [ 1468 | { 1469 | "name": "Bernhard Schussek", 1470 | "email": "bschussek@gmail.com" 1471 | } 1472 | ], 1473 | "description": "Assertions to validate method input/output with nice error messages.", 1474 | "keywords": [ 1475 | "assert", 1476 | "check", 1477 | "validate" 1478 | ], 1479 | "time": "2018-12-25T11:19:39+00:00" 1480 | } 1481 | ], 1482 | "aliases": [], 1483 | "minimum-stability": "stable", 1484 | "stability-flags": [], 1485 | "prefer-stable": false, 1486 | "prefer-lowest": false, 1487 | "platform": [], 1488 | "platform-dev": [] 1489 | } 1490 | -------------------------------------------------------------------------------- /third_party/composer.phar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikeland73/graphp/5f55bbb2eae1e058f83e831baaaec728a9c10dfd/third_party/composer.phar --------------------------------------------------------------------------------