├── .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 | [](https://www.codefactor.io/repository/github/mikeland86/graphp) [](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 |
91 | Go to other controller
92 |
93 |
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 |
2 |
3 | Welcome page
4 |
16 |
17 |
18 |
19 |
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 | Type
7 | Name
8 | From Type
9 | To Type
10 |
11 |
12 |
13 |
14 |
15 | =$edge->getType()?>
16 | =$edge->getName()?>
17 | =GPNodeMap::getCLass($edge->getFromType())?>
18 | =GPNodeMap::getClass($edge->getToType())?>
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/sample_app/views/admin/explore_view.php:
--------------------------------------------------------------------------------
1 | Node Types
2 |
3 |
4 |
5 |
6 | Type
7 | Name
8 | Count
9 |
10 |
11 |
12 | $name): ?>
13 |
14 | =$id?>
15 | =$name?>
16 | =idx($counts, $id, 0)?>
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/sample_app/views/admin/node_type_view.php:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | ID
17 | Data
18 | Updated
19 | Edit
20 | Delete
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | =$node->getID()?>
29 |
30 |
31 | =substr($node->getJSONData(), 0, 128)?>
32 | =$node->getUpdated()?>
33 |
34 |
37 | Edit
38 |
39 |
40 |
41 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/sample_app/views/admin/node_view.php:
--------------------------------------------------------------------------------
1 |
2 | back to = get_class($node) ?> list
3 |
4 | = get_class($node) ?> ID = $node->getID() ?>
5 | Data:
6 |
7 |
8 |
9 |
10 | Key
11 | Value
12 | Indexed
13 | Unset
14 |
15 |
16 |
17 | getDataArray() as $key => $value): ?>
18 |
19 | =$key?>
20 | =is_array($value) ? json_encode($value) : $value?>
21 |
22 | =idx($node->getIndexedData(), $key) ? 'yes' : 'no'?>
23 |
24 |
25 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
49 |
50 | Edges:
51 |
52 |
53 |
54 |
55 | Edge
56 | To Node
57 | Delete
58 |
59 |
60 |
61 | getConnectedNodes($node::getEdgeTypes())
62 | as $e => $nodes): ?>
63 |
64 |
65 |
66 | =$node::getEdgeTypeByType($e)->getName().' - '.$e?>
67 |
68 |
69 |
70 | Link
71 |
72 | ID: =$conn_node->getID()?>
73 | type: =get_class($conn_node)?>
74 | =method_exists( $conn_node, '__toString' ) ? ' - '.$conn_node : ''?>
75 |
76 |
77 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
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 | =$content?>
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/sample_app/views/login_view.php:
--------------------------------------------------------------------------------
1 |
10 |
11 |
--------------------------------------------------------------------------------
/sample_app/views/one_post.php:
--------------------------------------------------------------------------------
1 |
2 | =$post->getText()?>
3 |
4 | Comments:
5 |
6 | getComments() as $comment):?>
7 | =$comment->getText()?>
8 |
9 |
10 |
--------------------------------------------------------------------------------
/sample_app/views/post_list.php:
--------------------------------------------------------------------------------
1 |
2 | New Post:
3 |
4 | Create
5 | =GPSecurity::csrf()?>
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
--------------------------------------------------------------------------------