diff --git a/seatmap-webapi/.gitignore b/seatmap-webapi/.gitignore
new file mode 100644
index 0000000..92900b3
--- /dev/null
+++ b/seatmap-webapi/.gitignore
@@ -0,0 +1,3 @@
+composer.phar
+vendor/
+data.db
\ No newline at end of file
diff --git a/seatmap-webapi/.htaccess b/seatmap-webapi/.htaccess
new file mode 100644
index 0000000..4c81be1
--- /dev/null
+++ b/seatmap-webapi/.htaccess
@@ -0,0 +1,5 @@
+
+ RewriteEngine on
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteRule ^(.*)$ api.php/$1 [QSA,L]
+
\ No newline at end of file
diff --git a/seatmap-webapi/CONTRIBUTING.md b/seatmap-webapi/CONTRIBUTING.md
new file mode 100644
index 0000000..46dfc73
--- /dev/null
+++ b/seatmap-webapi/CONTRIBUTING.md
@@ -0,0 +1,21 @@
+# Contributing to php-crud-api
+
+Pull requests are welcome.
+
+## Use phpfmt
+
+Please use "phpfmt" to ensure consistent formatting.
+
+## Run the tests
+
+Before you do a PR, you should ensure any new functionality has test cases and that all existing tests are succeeding.
+
+## Run the build
+
+Since this project is a single file application, you must ensure that classes are loaded in the correct order.
+This is only important for the "extends" and "implements" relations. The 'build.php' script appends the classes in
+alphabetical order (directories first). The path of the class that is extended or implemented (parent) must be above
+the extending or implementing (child) class when listing the contents of the 'src' directory in this order. If you
+get this order wrong you will see the build will fail with a "Class not found" error message. The solution is to
+rename the child class so that it starts with a later letter in the alphabet than the parent class or that you move
+the parent class to a subdirectory (directories are scanned first).
diff --git a/seatmap-webapi/Dockerfile b/seatmap-webapi/Dockerfile
new file mode 100644
index 0000000..bac12f0
--- /dev/null
+++ b/seatmap-webapi/Dockerfile
@@ -0,0 +1,15 @@
+FROM php:apache
+
+RUN docker-php-ext-install pdo pdo_mysql
+
+RUN apt-get update; \
+ apt-get install -y libpq5 libpq-dev; \
+ docker-php-ext-install pdo pdo_pgsql; \
+ apt-get autoremove --purge -y libpq-dev; \
+ apt-get clean ; \
+ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
+
+RUN a2enmod rewrite
+
+COPY api.php /var/www/html/api.php
+COPY .htaccess /var/www/html/.htaccess
diff --git a/seatmap-webapi/LICENSE b/seatmap-webapi/LICENSE
new file mode 100644
index 0000000..cc461d0
--- /dev/null
+++ b/seatmap-webapi/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Maurits van der Schee
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/seatmap-webapi/README.md b/seatmap-webapi/README.md
new file mode 100644
index 0000000..1cf2737
--- /dev/null
+++ b/seatmap-webapi/README.md
@@ -0,0 +1,1389 @@
+# PHP-CRUD-API
+
+Single file PHP 7 script that adds a REST API to a MySQL/MariaDB, PostgreSQL, SQL Server or SQLite database.
+
+NB: This is the [TreeQL](https://treeql.org) reference implementation in PHP.
+
+Related projects:
+
+ - [PHP-API-AUTH](https://github.com/mevdschee/php-api-auth): Single file PHP script that is an authentication provider for PHP-CRUD-API
+ - [PHP-SP-API](https://github.com/mevdschee/php-sp-api): Single file PHP script that adds a REST API to a SQL database.
+ - [PHP-CRUD-UI](https://github.com/mevdschee/php-crud-ui): Single file PHP script that adds a UI to a PHP-CRUD-API project.
+ - [VUE-CRUD-UI](https://github.com/nlware/vue-crud-ui): Single file Vue.js script that adds a UI to a PHP-CRUD-API project.
+ - [PHP-CRUD-ADMIN](https://github.com/mevdschee/php-crud-admin): Single file PHP script that adds a database admin interface to a PHP-CRUD-API project.
+
+There are also ports of this script in:
+
+- [Java JDBC by Ivan Kolchagov](https://github.com/kolchagov/java-crud-api) (v1)
+- [Java Spring Boot + jOOQ](https://github.com/mevdschee/java-crud-api/tree/master/full) (v2: work in progress)
+
+There are also proof-of-concept ports of this script that only support basic REST CRUD functionality in:
+[PHP](https://github.com/mevdschee/php-crud-api/blob/master/extras/core.php),
+[Java](https://github.com/mevdschee/java-crud-api/blob/master/core/src/main/java/com/tqdev/CrudApiHandler.java),
+[Go](https://github.com/mevdschee/go-crud-api/blob/master/api.go),
+[C# .net core](https://github.com/mevdschee/core-data-api/blob/master/Program.cs),
+[Node.js](https://github.com/mevdschee/js-crud-api/blob/master/app.js) and
+[Python](https://github.com/mevdschee/py-crud-api/blob/master/api.py).
+
+## Requirements
+
+ - PHP 7.0 or higher with PDO drivers enabled for one of these database systems:
+ - MySQL 5.6 / MariaDB 10.0 or higher for spatial features in MySQL
+ - PostgreSQL 9.1 or higher with PostGIS 2.0 or higher for spatial features
+ - SQL Server 2012 or higher (2017 for Linux support)
+ - SQLite 3.16 or higher (spatial features NOT supported)
+
+## Installation
+
+This is a single file application! Upload "`api.php`" somewhere and enjoy!
+
+For local development you may run PHP's built-in web server:
+
+ php -S localhost:8080
+
+Test the script by opening the following URL:
+
+ http://localhost:8080/api.php/records/posts/1
+
+Don't forget to modify the configuration at the bottom of the file.
+
+Alternatively you can integrate this project into the web framework of your choice, see:
+
+- [Automatic REST API for Laravel](https://tqdev.com/2019-automatic-rest-api-laravel)
+- [Automatic REST API for Symfony 4](https://tqdev.com/2019-automatic-rest-api-symfony)
+- [Automatic REST API for SlimPHP 4](https://tqdev.com/2019-automatic-api-slimphp-4)
+
+In these integrations [Composer](https://getcomposer.org/) is used to load this project as a dependency.
+
+For people that don't use composer, the file "`api.include.php`" is provided. This file contains everything
+from "`api.php`" except the configuration from "`src/index.php`" and can be used by PHP's "include".
+
+## Configuration
+
+Edit the following lines in the bottom of the file "`api.php`":
+
+ $config = new Config([
+ 'username' => 'xxx',
+ 'password' => 'xxx',
+ 'database' => 'xxx',
+ ]);
+
+These are all the configuration options and their default value between brackets:
+
+- "driver": `mysql`, `pgsql`, `sqlsrv` or `sqlite` (`mysql`)
+- "address": Hostname (or filename) of the database server (`localhost`)
+- "port": TCP port of the database server (defaults to driver default)
+- "username": Username of the user connecting to the database (no default)
+- "password": Password of the user connecting to the database (no default)
+- "database": Database the connecting is made to (no default)
+- "tables": Comma separated list of tables to publish (defaults to 'all')
+- "middlewares": List of middlewares to load (`cors`)
+- "controllers": List of controllers to load (`records,geojson,openapi`)
+- "openApiBase": OpenAPI info (`{"info":{"title":"PHP-CRUD-API","version":"1.0.0"}}`)
+- "cacheType": `TempFile`, `Redis`, `Memcache`, `Memcached` or `NoCache` (`TempFile`)
+- "cachePath": Path/address of the cache (defaults to system's temp directory)
+- "cacheTime": Number of seconds the cache is valid (`10`)
+- "debug": Show errors in the "X-Exception" headers (`false`)
+- "basePath": URI base path of the API (determined using PATH_INFO by default)
+
+All configuration options are also available as environment variables. Write the config option with capitals, a "PHP_CRUD_API_" prefix and underscores for word breakes, so for instance:
+
+- PHP_CRUD_API_DRIVER=mysql
+- PHP_CRUD_API_ADDRESS=localhost
+- PHP_CRUD_API_PORT=3306
+- PHP_CRUD_API_DATABASE=php-crud-api
+- PHP_CRUD_API_USERNAME=php-crud-api
+- PHP_CRUD_API_PASSWORD=php-crud-api
+- PHP_CRUD_API_DEBUG=1
+
+The environment variables take precedence over the PHP configuration.
+
+## Limitations
+
+These limitation and constrains apply:
+
+ - Primary keys should either be auto-increment (from 1 to 2^53) or UUID
+ - Composite primary and composite foreign keys are not supported
+ - Complex writes (transactions) are not supported
+ - Complex queries calling functions (like "concat" or "sum") are not supported
+ - Database must support and define foreign key constraints
+ - SQLite cannot have bigint typed auto incrementing primary keys
+ - SQLite does not support altering table columns (structure)
+
+## Features
+
+The following features are supported:
+
+ - Composer install or single PHP file, easy to deploy.
+ - Very little code, easy to adapt and maintain
+ - Supports POST variables as input (x-www-form-urlencoded)
+ - Supports a JSON object as input
+ - Supports a JSON array as input (batch insert)
+ - Sanitize and validate input using type rules and callbacks
+ - Permission system for databases, tables, columns and records
+ - Multi-tenant single and multi database layouts are supported
+ - Multi-domain CORS support for cross-domain requests
+ - Support for reading joined results from multiple tables
+ - Search support on multiple criteria
+ - Pagination, sorting, top N list and column selection
+ - Relation detection with nested results (belongsTo, hasMany and HABTM)
+ - Atomic increment support via PATCH (for counters)
+ - Binary fields supported with base64 encoding
+ - Spatial/GIS fields and filters supported with WKT and GeoJSON
+ - Generate API documentation using OpenAPI tools
+ - Authentication via JWT token or username/password
+ - Database connection parameters may depend on authentication
+ - Support for reading database structure in JSON
+ - Support for modifying database structure using REST endpoint
+ - Security enhancing middleware is included
+ - Standard compliant: PSR-4, PSR-7, PSR-12, PSR-15 and PSR-17
+
+## Compilation
+
+You can install all dependencies of this project using the following command:
+
+ php install.php
+
+You can compile all files into a single "`api.php`" file using:
+
+ php build.php
+
+NB: The install script will patch the dependencies in the vendor directory for PHP 7.0 compatibility.
+
+### Development
+
+You can access the non-compiled code at the URL:
+
+ http://localhost:8080/src/records/posts/1
+
+The non-compiled code resides in the "`src`" and "`vendor`" directories. The "`vendor`" directory contains the dependencies.
+
+### Updating dependencies
+
+You can update all dependencies of this project using the following command:
+
+ php update.php
+
+This script will install and run [Composer](https://getcomposer.org/) to update the dependencies.
+
+NB: The update script will patch the dependencies in the vendor directory for PHP 7.0 compatibility.
+
+## TreeQL, a pragmatic GraphQL
+
+[TreeQL](https://treeql.org) allows you to create a "tree" of JSON objects based on your SQL database structure (relations) and your query.
+
+It is loosely based on the REST standard and also inspired by json:api.
+
+### CRUD + List
+
+The example posts table has only a a few fields:
+
+ posts
+ =======
+ id
+ title
+ content
+ created
+
+The CRUD + List operations below act on this table.
+
+#### Create
+
+If you want to create a record the request can be written in URL format as:
+
+ POST /records/posts
+
+You have to send a body containing:
+
+ {
+ "title": "Black is the new red",
+ "content": "This is the second post.",
+ "created": "2018-03-06T21:34:01Z"
+ }
+
+And it will return the value of the primary key of the newly created record:
+
+ 2
+
+#### Read
+
+To read a record from this table the request can be written in URL format as:
+
+ GET /records/posts/1
+
+Where "1" is the value of the primary key of the record that you want to read. It will return:
+
+ {
+ "id": 1
+ "title": "Hello world!",
+ "content": "Welcome to the first post.",
+ "created": "2018-03-05T20:12:56Z"
+ }
+
+On read operations you may apply joins.
+
+#### Update
+
+To update a record in this table the request can be written in URL format as:
+
+ PUT /records/posts/1
+
+Where "1" is the value of the primary key of the record that you want to update. Send as a body:
+
+ {
+ "title": "Adjusted title!"
+ }
+
+This adjusts the title of the post. And the return value is the number of rows that are set:
+
+ 1
+
+#### Delete
+
+If you want to delete a record from this table the request can be written in URL format as:
+
+ DELETE /records/posts/1
+
+And it will return the number of deleted rows:
+
+ 1
+
+#### List
+
+To list records from this table the request can be written in URL format as:
+
+ GET /records/posts
+
+It will return:
+
+ {
+ "records":[
+ {
+ "id": 1,
+ "title": "Hello world!",
+ "content": "Welcome to the first post.",
+ "created": "2018-03-05T20:12:56Z"
+ }
+ ]
+ }
+
+On list operations you may apply filters and joins.
+
+### Filters
+
+Filters provide search functionality, on list calls, using the "filter" parameter. You need to specify the column
+name, a comma, the match type, another commma and the value you want to filter on. These are supported match types:
+
+ - "cs": contain string (string contains value)
+ - "sw": start with (string starts with value)
+ - "ew": end with (string end with value)
+ - "eq": equal (string or number matches exactly)
+ - "lt": lower than (number is lower than value)
+ - "le": lower or equal (number is lower than or equal to value)
+ - "ge": greater or equal (number is higher than or equal to value)
+ - "gt": greater than (number is higher than value)
+ - "bt": between (number is between two comma separated values)
+ - "in": in (number or string is in comma separated list of values)
+ - "is": is null (field contains "NULL" value)
+
+You can negate all filters by prepending a "n" character, so that "eq" becomes "neq".
+Examples of filter usage are:
+
+ GET /records/categories?filter=name,eq,Internet
+ GET /records/categories?filter=name,sw,Inter
+ GET /records/categories?filter=id,le,1
+ GET /records/categories?filter=id,ngt,1
+ GET /records/categories?filter=id,bt,0,1
+ GET /records/categories?filter=id,in,0,1
+
+Output:
+
+ {
+ "records":[
+ {
+ "id": 1
+ "name": "Internet"
+ }
+ ]
+ }
+
+In the next section we dive deeper into how you can apply multiple filters on a single list call.
+
+### Multiple filters
+
+Filters can be a by applied by repeating the "filter" parameter in the URL. For example the following URL:
+
+ GET /records/categories?filter=id,gt,1&filter=id,lt,3
+
+will request all categories "where id > 1 and id < 3". If you wanted "where id = 2 or id = 4" you should write:
+
+ GET /records/categories?filter1=id,eq,2&filter2=id,eq,4
+
+As you see we added a number to the "filter" parameter to indicate that "OR" instead of "AND" should be applied.
+Note that you can also repeat "filter1" and create an "AND" within an "OR". Since you can also go one level deeper
+by adding a letter (a-f) you can create almost any reasonably complex condition tree.
+
+NB: You can only filter on the requested table (not on it's included tables) and filters are only applied on list calls.
+
+### Column selection
+
+By default all columns are selected. With the "include" parameter you can select specific columns.
+You may use a dot to separate the table name from the column name. Multiple columns should be comma separated.
+An asterisk ("*") may be used as a wildcard to indicate "all columns". Similar to "include" you may use the "exclude" parameter to remove certain columns:
+
+```
+GET /records/categories/1?include=name
+GET /records/categories/1?include=categories.name
+GET /records/categories/1?exclude=categories.id
+```
+
+Output:
+
+```
+ {
+ "name": "Internet"
+ }
+```
+
+NB: Columns that are used to include related entities are automatically added and cannot be left out of the output.
+
+### Ordering
+
+With the "order" parameter you can sort. By default the sort is in ascending order, but by specifying "desc" this can be reversed:
+
+```
+GET /records/categories?order=name,desc
+GET /records/categories?order=id,desc&order=name
+```
+
+Output:
+
+```
+ {
+ "records":[
+ {
+ "id": 3
+ "name": "Web development"
+ },
+ {
+ "id": 1
+ "name": "Internet"
+ }
+ ]
+ }
+```
+
+NB: You may sort on multiple fields by using multiple "order" parameters. You can not order on "joined" columns.
+
+### Limit size
+
+The "size" parameter limits the number of returned records. This can be used for top N lists together with the "order" parameter (use descending order).
+
+```
+GET /records/categories?order=id,desc&size=1
+```
+
+Output:
+
+```
+ {
+ "records":[
+ {
+ "id": 3
+ "name": "Web development"
+ }
+ ]
+ }
+```
+
+NB: If you also want to know to the total number of records you may want to use the "page" parameter.
+
+### Pagination
+
+The "page" parameter holds the requested page. The default page size is 20, but can be adjusted (e.g. to 50).
+
+```
+GET /records/categories?order=id&page=1
+GET /records/categories?order=id&page=1,50
+```
+
+Output:
+
+```
+ {
+ "records":[
+ {
+ "id": 1
+ "name": "Internet"
+ },
+ {
+ "id": 3
+ "name": "Web development"
+ }
+ ],
+ "results": 2
+ }
+```
+
+NB: Since pages that are not ordered cannot be paginated, pages will be ordered by primary key.
+
+### Joins
+
+Let's say that you have a posts table that has comments (made by users) and the posts can have tags.
+
+ posts comments users post_tags tags
+ ======= ======== ======= ========= =======
+ id id id id id
+ title post_id username post_id name
+ content user_id phone tag_id
+ created message
+
+When you want to list posts with their comments users and tags you can ask for two "tree" paths:
+
+ posts -> comments -> users
+ posts -> post_tags -> tags
+
+These paths have the same root and this request can be written in URL format as:
+
+ GET /records/posts?join=comments,users&join=tags
+
+Here you are allowed to leave out the intermediate table that binds posts to tags. In this example
+you see all three table relation types (hasMany, belongsTo and hasAndBelongsToMany) in effect:
+
+- "post" has many "comments"
+- "comment" belongs to "user"
+- "post" has and belongs to many "tags"
+
+This may lead to the following JSON data:
+
+ {
+ "records":[
+ {
+ "id": 1,
+ "title": "Hello world!",
+ "content": "Welcome to the first post.",
+ "created": "2018-03-05T20:12:56Z",
+ "comments": [
+ {
+ id: 1,
+ post_id: 1,
+ user_id: {
+ id: 1,
+ username: "mevdschee",
+ phone: null,
+ },
+ message: "Hi!"
+ },
+ {
+ id: 2,
+ post_id: 1,
+ user_id: {
+ id: 1,
+ username: "mevdschee",
+ phone: null,
+ },
+ message: "Hi again!"
+ }
+ ],
+ "tags": []
+ },
+ {
+ "id": 2,
+ "title": "Black is the new red",
+ "content": "This is the second post.",
+ "created": "2018-03-06T21:34:01Z",
+ "comments": [],
+ "tags": [
+ {
+ id: 1,
+ message: "Funny"
+ },
+ {
+ id: 2,
+ message: "Informational"
+ }
+ ]
+ }
+ ]
+ }
+
+You see that the "belongsTo" relationships are detected and the foreign key value is replaced by the referenced object.
+In case of "hasMany" and "hasAndBelongsToMany" the table name is used a new property on the object.
+
+### Batch operations
+
+When you want to create, read, update or delete you may specify multiple primary key values in the URL.
+You also need to send an array instead of an object in the request body for create and update.
+
+To read a record from this table the request can be written in URL format as:
+
+ GET /records/posts/1,2
+
+The result may be:
+
+ [
+ {
+ "id": 1,
+ "title": "Hello world!",
+ "content": "Welcome to the first post.",
+ "created": "2018-03-05T20:12:56Z"
+ },
+ {
+ "id": 2,
+ "title": "Black is the new red",
+ "content": "This is the second post.",
+ "created": "2018-03-06T21:34:01Z"
+ }
+ ]
+
+Similarly when you want to do a batch update the request in URL format is written as:
+
+ PUT /records/posts/1,2
+
+Where "1" and "2" are the values of the primary keys of the records that you want to update. The body should
+contain the same number of objects as there are primary keys in the URL:
+
+ [
+ {
+ "title": "Adjusted title for ID 1"
+ },
+ {
+ "title": "Adjusted title for ID 2"
+ }
+ ]
+
+This adjusts the titles of the posts. And the return values are the number of rows that are set:
+
+ 1,1
+
+Which means that there were two update operations and each of them had set one row. Batch operations use database
+transactions, so they either all succeed or all fail (successful ones get roled back).
+
+### Spatial support
+
+For spatial support there is an extra set of filters that can be applied on geometry columns and that starting with an "s":
+
+ - "sco": spatial contains (geometry contains another)
+ - "scr": spatial crosses (geometry crosses another)
+ - "sdi": spatial disjoint (geometry is disjoint from another)
+ - "seq": spatial equal (geometry is equal to another)
+ - "sin": spatial intersects (geometry intersects another)
+ - "sov": spatial overlaps (geometry overlaps another)
+ - "sto": spatial touches (geometry touches another)
+ - "swi": spatial within (geometry is within another)
+ - "sic": spatial is closed (geometry is closed and simple)
+ - "sis": spatial is simple (geometry is simple)
+ - "siv": spatial is valid (geometry is valid)
+
+These filters are based on OGC standards and so is the WKT specification in which the geometry columns are represented.
+
+#### GeoJSON
+
+The GeoJSON support is a read-only view on the tables and records in GeoJSON format. These requests are supported:
+
+ method path - operation - description
+ ----------------------------------------------------------------------------------------
+ GET /geojson/{table} - list - lists records as a GeoJSON FeatureCollection
+ GET /geojson/{table}/{id} - read - reads a record by primary key as a GeoJSON Feature
+
+The "`/geojson`" endpoint uses the "`/records`" endpoint internally and inherits all functionality, such as joins and filters.
+It also supports a "geometry" parameter to indicate the name of the geometry column in case the table has more than one.
+For map views it supports the "bbox" parameter in which you can specify upper-left and lower-right coordinates (comma separated).
+The following Geometry types are supported by the GeoJSON implementation:
+
+ - Point
+ - MultiPoint
+ - LineString
+ - MultiLineString
+ - Polygon
+ - MultiPolygon
+
+The GeoJSON functionality is enabled by default, but can be disabled using the "controllers" configuration.
+
+## Middleware
+
+You can enable the following middleware using the "middlewares" config parameter:
+
+- "firewall": Limit access to specific IP addresses
+- "sslRedirect": Force connection over HTTPS instead of HTTP
+- "cors": Support for CORS requests (enabled by default)
+- "xsrf": Block XSRF attacks using the 'Double Submit Cookie' method
+- "ajaxOnly": Restrict non-AJAX requests to prevent XSRF attacks
+- "dbAuth": Support for "Database Authentication"
+- "jwtAuth": Support for "JWT Authentication"
+- "basicAuth": Support for "Basic Authentication"
+- "reconnect": Reconnect to the database with different parameters
+- "authorization": Restrict access to certain tables or columns
+- "validation": Return input validation errors for custom rules and default type rules
+- "ipAddress": Fill a protected field with the IP address on create
+- "sanitation": Apply input sanitation on create and update
+- "multiTenancy": Restricts tenants access in a multi-tenant scenario
+- "pageLimits": Restricts list operations to prevent database scraping
+- "joinLimits": Restricts join parameters to prevent database scraping
+- "customization": Provides handlers for request and response customization
+- "xml": Translates all input and output from JSON to XML
+
+The "middlewares" config parameter is a comma separated list of enabled middlewares.
+You can tune the middleware behavior using middleware specific configuration parameters:
+
+- "firewall.reverseProxy": Set to "true" when a reverse proxy is used ("")
+- "firewall.allowedIpAddresses": List of IP addresses that are allowed to connect ("")
+- "cors.allowedOrigins": The origins allowed in the CORS headers ("*")
+- "cors.allowHeaders": The headers allowed in the CORS request ("Content-Type, X-XSRF-TOKEN, X-Authorization")
+- "cors.allowMethods": The methods allowed in the CORS request ("OPTIONS, GET, PUT, POST, DELETE, PATCH")
+- "cors.allowCredentials": To allow credentials in the CORS request ("true")
+- "cors.exposeHeaders": Whitelist headers that browsers are allowed to access ("")
+- "cors.maxAge": The time that the CORS grant is valid in seconds ("1728000")
+- "xsrf.excludeMethods": The methods that do not require XSRF protection ("OPTIONS,GET")
+- "xsrf.cookieName": The name of the XSRF protection cookie ("XSRF-TOKEN")
+- "xsrf.headerName": The name of the XSRF protection header ("X-XSRF-TOKEN")
+- "ajaxOnly.excludeMethods": The methods that do not require AJAX ("OPTIONS,GET")
+- "ajaxOnly.headerName": The name of the required header ("X-Requested-With")
+- "ajaxOnly.headerValue": The value of the required header ("XMLHttpRequest")
+- "dbAuth.mode": Set to "optional" if you want to allow anonymous access ("required")
+- "dbAuth.usersTable": The table that is used to store the users in ("users")
+- "dbAuth.usernameColumn": The users table column that holds usernames ("username")
+- "dbAuth.passwordColumn": The users table column that holds passwords ("password")
+- "dbAuth.returnedColumns": The columns returned on successful login, empty means 'all' ("")
+- "dbAuth.registerUser": JSON user data (or "1") in case you want the /register endpoint enabled ("")
+- "dbAuth.passwordLength": Minimum length that the password must have ("12")
+- "dbAuth.sessionName": The name of the PHP session that is started ("")
+- "jwtAuth.mode": Set to "optional" if you want to allow anonymous access ("required")
+- "jwtAuth.header": Name of the header containing the JWT token ("X-Authorization")
+- "jwtAuth.leeway": The acceptable number of seconds of clock skew ("5")
+- "jwtAuth.ttl": The number of seconds the token is valid ("30")
+- "jwtAuth.secrets": The shared secret(s) used to sign the JWT token with ("")
+- "jwtAuth.algorithms": The algorithms that are allowed, empty means 'all' ("")
+- "jwtAuth.audiences": The audiences that are allowed, empty means 'all' ("")
+- "jwtAuth.issuers": The issuers that are allowed, empty means 'all' ("")
+- "jwtAuth.sessionName": The name of the PHP session that is started ("")
+- "basicAuth.mode": Set to "optional" if you want to allow anonymous access ("required")
+- "basicAuth.realm": Text to prompt when showing login ("Username and password required")
+- "basicAuth.passwordFile": The file to read for username/password combinations (".htpasswd")
+- "basicAuth.sessionName": The name of the PHP session that is started ("")
+- "reconnect.driverHandler": Handler to implement retrieval of the database driver ("")
+- "reconnect.addressHandler": Handler to implement retrieval of the database address ("")
+- "reconnect.portHandler": Handler to implement retrieval of the database port ("")
+- "reconnect.databaseHandler": Handler to implement retrieval of the database name ("")
+- "reconnect.tablesHandler": Handler to implement retrieval of the table names ("")
+- "reconnect.usernameHandler": Handler to implement retrieval of the database username ("")
+- "reconnect.passwordHandler": Handler to implement retrieval of the database password ("")
+- "authorization.tableHandler": Handler to implement table authorization rules ("")
+- "authorization.columnHandler": Handler to implement column authorization rules ("")
+- "authorization.pathHandler": Handler to implement path authorization rules ("")
+- "authorization.recordHandler": Handler to implement record authorization filter rules ("")
+- "validation.handler": Handler to implement validation rules for input values ("")
+- "validation.types": Types to enable type validation for, empty means 'none' ("all")
+- "validation.tables": Tables to enable type validation for, empty means 'none' ("all")
+- "ipAddress.tables": Tables to search for columns to override with IP address ("")
+- "ipAddress.columns": Columns to protect and override with the IP address on create ("")
+- "sanitation.handler": Handler to implement sanitation rules for input values ("")
+- "sanitation.types": Types to enable type sanitation for, empty means 'none' ("all")
+- "sanitation.tables": Tables to enable type sanitation for, empty means 'none' ("all")
+- "multiTenancy.handler": Handler to implement simple multi-tenancy rules ("")
+- "pageLimits.pages": The maximum page number that a list operation allows ("100")
+- "pageLimits.records": The maximum number of records returned by a list operation ("1000")
+- "joinLimits.depth": The maximum depth (length) that is allowed in a join path ("3")
+- "joinLimits.tables": The maximum number of tables that you are allowed to join ("10")
+- "joinLimits.records": The maximum number of records returned for a joined entity ("1000")
+- "customization.beforeHandler": Handler to implement request customization ("")
+- "customization.afterHandler": Handler to implement response customization ("")
+- "xml.types": JSON types that should be added to the XML type attribute ("null,array")
+
+If you don't specify these parameters in the configuration, then the default values (between brackets) are used.
+
+In the sections below you find more information on the built-in middleware.
+
+### Authentication
+
+Currently there are three types of authentication supported. They all store the authenticated user in the `$_SESSION` super global.
+This variable can be used in the authorization handlers to decide wether or not sombeody should have read or write access to certain tables, columns or records.
+The following overview shows the kinds of authentication middleware that you can enable.
+
+| Name | Middleware | Authenticated via | Users are stored in | Session variable |
+| -------- | ---------- | ---------------------- | ------------------- | ----------------------- |
+| Database | dbAuth | '/login' endpoint | database table | `$_SESSION['user']` |
+| Basic | basicAuth | 'Authorization' header | '.htpasswd' file | `$_SESSION['username']` |
+| JWT | jwtAuth | 'Authorization' header | identity provider | `$_SESSION['claims']` |
+
+Below you find more information on each of the authentication types.
+
+#### Database authentication
+
+The database authentication middleware defines three new routes:
+
+ method path - parameters - description
+ ---------------------------------------------------------------------------------------------------
+ GET /me - - returns the user that is currently logged in
+ POST /register - username, password - adds a user with given username and password
+ POST /login - username, password - logs a user in by username and password
+ POST /password - username, password, newPassword - updates the password of the logged in user
+ POST /logout - - logs out the currently logged in user
+
+A user can be logged in by sending it's username and password to the login endpoint (in JSON format).
+The authenticated user (with all it's properties) will be stored in the `$_SESSION['user']` variable.
+The user can be logged out by sending a POST request with an empty body to the logout endpoint.
+The passwords are stored as hashes in the password column in the users table. You can register a new user
+using the register endpoint, but this functionality must be turned on using the "dbAuth.regsiterUser"
+configuration parameter.
+
+It is IMPORTANT to restrict access to the users table using the 'authorization' middleware, otherwise all
+users can freely add, modify or delete any account! The minimal configuration is shown below:
+
+ 'middlewares' => 'dbAuth,authorization',
+ 'authorization.tableHandler' => function ($operation, $tableName) {
+ return $tableName != 'users';
+ },
+
+Note that this middleware uses session cookies and stores the logged in state on the server.
+
+#### Basic authentication
+
+The Basic type supports a file (by default '.htpasswd') that holds the users and their (hashed) passwords separated by a colon (':').
+When the passwords are entered in plain text they fill be automatically hashed.
+The authenticated username will be stored in the `$_SESSION['username']` variable.
+You need to send an "Authorization" header containing a base64 url encoded and colon separated username and password after the word "Basic".
+
+ Authorization: Basic dXNlcm5hbWUxOnBhc3N3b3JkMQ
+
+This example sends the string "username1:password1".
+
+#### JWT authentication
+
+The JWT type requires another (SSO/Identity) server to sign a token that contains claims.
+Both servers share a secret so that they can either sign or verify that the signature is valid.
+Claims are stored in the `$_SESSION['claims']` variable. You need to send an "X-Authorization"
+header containing a base64 url encoded and dot separated token header, body and signature after
+the word "Bearer" ([read more about JWT here](https://jwt.io/)). The standard says you need to
+use the "Authorization" header, but this is problematic in Apache and PHP.
+
+ X-Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6IjE1MzgyMDc2MDUiLCJleHAiOjE1MzgyMDc2MzV9.Z5px_GT15TRKhJCTHhDt5Z6K6LRDSFnLj8U5ok9l7gw
+
+This example sends the signed claims:
+
+ {
+ "sub": "1234567890",
+ "name": "John Doe",
+ "admin": true,
+ "iat": "1538207605",
+ "exp": 1538207635
+ }
+
+NB: The JWT implementation only supports the RSA and HMAC based algorithms.
+
+##### Configure and test JWT authentication with Auth0
+
+First you need to create an account on [Auth0](https://auth0.com/auth/login).
+Once logged in, you have to create an application (its type does not matter). Collect the `Domain`
+and `Client ID` and keep them for a later use. Then, create an API: give it a name and fill the
+`identifier` field with your API endpoint's URL.
+
+Then you have to configure the `jwtAuth.secrets` configuration in your `api.php` file.
+Don't fill it with the `secret` you will find in your Auth0 application settings but with **a
+public certificate**. To find it, go to the settings of your application, then in "Extra settings".
+You will now find a "Certificates" tab where you will find your Public Key in the Signing
+Certificate field.
+
+To test your integration, you can copy the [auth0/vanilla.html](examples/clients/auth0/vanilla.html)
+file. Be sure to fill these three variables:
+
+ - `authUrl` with your Auth0 domain
+ - `clientId` with your Client ID
+ - `audience` with the API URL you created in Auth0
+
+⚠️ If you don't fill the audience parameter, it will not work because you won't get a valid JWT.
+
+You can also change the `url` variable, used to test the API with authentication.
+
+[More info](https://auth0.com/docs/api-auth/tutorials/verify-access-token)
+
+##### Configure and test JWT authentication with Firebase
+
+First you need to create a Firebase project on the [Firebase console](https://console.firebase.google.com/).
+Add a web application to this project and grab the code snippet for later use.
+
+Then you have to configure the `jwtAuth.secrets` configuration in your `api.php` file.
+This can be done as follows:
+
+a. Log a user in to your Firebase-based app, get an authentication token for that user
+b. Go to [https://jwt.io/](https://jwt.io/) and paste the token in the decoding field
+c. Read the decoded header information from the token, it will give you the correct `kid`
+d. Grab the public key via this [URL](https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com), which corresponds to your `kid` from previous step
+e. Now, just fill `jwtAuth.secrets` with your public key in the `api.php`
+
+Here is an example of what it should look like in the configuration:
+
+```
+...,
+'middlewares' => 'cors, jwtAuth, authorization',
+ 'jwtAuth.secrets' => "ce5ced6e40dcd1eff407048867b1ed1e706686a0:-----BEGIN CERTIFICATE-----\nMIIDHDCCAgSgAwIBAgIIExun9bJSK1wwDQYJKoZIhvcNAQEFBQAwMTEvMC0GA1UE\nAxMmc2VjdXJldG9rZW4uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wHhcNMTkx\nMjIyMjEyMTA3WhcNMjAwMTA4MDkzNjA3WjAxMS8wLQYDVQQDEyZzZWN1cmV0b2tl\nbi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBAKsvVDUwXeYQtySNvyI1/tZAk0sj7Zx4/1+YLUomwlK6vmEd\nyl2IXOYOj3VR7FBA24A9//nnrp+mV8YOYEOdaWX7PQo0PIPFPqdA0r7CqBUWHPfQ\n1WVHVRQY3G0c7upM97UfMes9xOrMqyvecMRk1e5S6eT12Zh2og7yiVs8gP83M1EB\nGqseUaltaadjyT35w5B0Ny0/7NdLYiv2G6Z0S821SxvSo1/wfmilnBBKYYluP0PA\n9NPznWFP6uXnX7gKxyJT9//cYVxTO6+b1TT13Yvrpm1a4EuCOhLrZH6ErHQTccAM\nhAx8mdNtbROsp0dlPKrSfqO82uFz45RXZYmSeP0CAwEAAaM4MDYwDAYDVR0TAQH/\nBAIwADAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwIwDQYJ\nKoZIhvcNAQEFBQADggEBACNsJ5m00gdTvD6j6ahURsGrNZ0VJ0YREVQ5U2Jtubr8\nn2fuhMxkB8147ISzfi6wZR+yNwPGjlr8JkAHAC0i+Nam9SqRyfZLqsm+tHdgFT8h\npa+R/FoGrrLzxJNRiv0Trip8hZjgz3PClz6KxBQzqL+rfGV2MbwTXuBoEvLU1mYA\no3/UboJT7cNGjZ8nHXeoKMsec1/H55lUdconbTm5iMU1sTDf+3StGYzTwC+H6yc2\nY3zIq3/cQUCrETkALrqzyCnLjRrLYZu36ITOaKUbtmZhwrP99i2f+H4Ab2i8jeMu\nk61HD29mROYjl95Mko2BxL+76To7+pmn73U9auT+xfA=\n-----END CERTIFICATE-----\n",
+ 'cors.allowedOrigins' => '*',
+ 'cors.allowHeaders' => 'X-Authorization'
+```
+
+Notes:
+ - The `kid:key` pair is formatted as a string
+ - Do not include spaces before or after the ':'
+ - Use double quotation marks (") around the string text
+ - The string must contain the linefeeds (\n)
+
+To test your integration, you can copy the [firebase/vanilla.html](examples/clients/firebase/vanilla.html)
+file and the [firebase/vanilla-success.html](examples/clients/firebase/vanilla-success.html) file,
+used as a "success" page and to display the API result.
+
+Replace, in both files, the Firebase configuration (`firebaseConfig` object).
+
+You can also change the `url` variable, used to test the API with authentication.
+
+[More info](https://firebase.google.com/docs/auth/admin/verify-id-tokens#verify_id_tokens_using_a_third-party_jwt_library)
+
+### Authorizing operations
+
+The Authorization model acts on "operations". The most important ones are listed here:
+
+ method path - operation - description
+ ----------------------------------------------------------------------------------------
+ GET /records/{table} - list - lists records
+ POST /records/{table} - create - creates records
+ GET /records/{table}/{id} - read - reads a record by primary key
+ PUT /records/{table}/{id} - update - updates columns of a record by primary key
+ DELETE /records/{table}/{id} - delete - deletes a record by primary key
+ PATCH /records/{table}/{id} - increment - increments columns of a record by primary key
+
+The "`/openapi`" endpoint will only show what is allowed in your session. It also has a special
+"document" operation to allow you to hide tables and columns from the documentation.
+
+For endpoints that start with "`/columns`" there are the operations "reflect" and "remodel".
+These operations can display or change the definition of the database, table or column.
+This functionality is disabled by default and for good reason (be careful!).
+Add the "columns" controller in the configuration to enable this functionality.
+
+### Authorizing tables, columns and records
+
+By default all tables, columns and paths are accessible. If you want to restrict access to some tables you may add the 'authorization' middleware
+and define a 'authorization.tableHandler' function that returns 'false' for these tables.
+
+ 'authorization.tableHandler' => function ($operation, $tableName) {
+ return $tableName != 'license_keys';
+ },
+
+The above example will restrict access to the table 'license_keys' for all operations.
+
+ 'authorization.columnHandler' => function ($operation, $tableName, $columnName) {
+ return !($tableName == 'users' && $columnName == 'password');
+ },
+
+The above example will restrict access to the 'password' field of the 'users' table for all operations.
+
+ 'authorization.recordHandler' => function ($operation, $tableName) {
+ return ($tableName == 'users') ? 'filter=username,neq,admin' : '';
+ },
+
+The above example will disallow access to user records where the username is 'admin'.
+This construct adds a filter to every executed query.
+
+ 'authorization.pathHandler' => function ($path) {
+ return $path === 'openapi' ? false : true;
+ },
+
+The above example will disabled the `/openapi` route.
+
+NB: You need to handle the creation of invalid records with a validation (or sanitation) handler.
+
+### SQL GRANT authorization
+
+You can alternatively use database permissons (SQL GRANT statements) to define the authorization model. In this case you
+should not use the "authorization" middleware, but you do need to use the "reconnect" middleware. The handlers of the
+"reconnect" middleware allow you to specify the correct username and password, like this:
+
+ 'reconnect.usernameHandler' => function () {
+ return 'mevdschee';
+ },
+ 'reconnect.passwordHandler' => function () {
+ return 'secret123';
+ },
+
+This will make the API connect to the database specifying "mevdschee" as the username and "secret123" as the password.
+The OpenAPI specification is less specific on allowed and disallowed operations when you are using database permissions,
+as the permissions are not read in the reflection step.
+
+NB: You may want to retrieve the username and password from the session (the "$_SESSION" variable).
+
+### Sanitizing input
+
+By default all input is accepted and sent to the database. If you want to strip (certain) HTML tags before storing you may add
+the 'sanitation' middleware and define a 'sanitation.handler' function that returns the adjusted value.
+
+ 'sanitation.handler' => function ($operation, $tableName, $column, $value) {
+ return is_string($value) ? strip_tags($value) : $value;
+ },
+
+The above example will strip all HTML tags from strings in the input.
+
+### Type sanitation
+
+If you enable the 'sanitation' middleware, then you (automatically) also enable type sanitation. When this is enabled you may:
+
+- send leading and trailing whitespace in a non-character field (it will be ignored).
+- send a float to an integer or bigint field (it will be rounded).
+- send a base64url encoded string (it will be converted to regular base64 encoding).
+- send a time/date/timestamp in any [strtotime accepted format](https://www.php.net/manual/en/datetime.formats.php) (it will be converted).
+
+You may use the config settings "`sanitation.types`" and "`sanitation.tables`"' to define for which types and
+in which tables you want to apply type sanitation (defaults to 'all'). Example:
+
+ 'sanitation.types' => 'date,timestamp',
+ 'sanitation.tables' => 'posts,comments',
+
+Here we enable the type sanitation for date and timestamp fields in the posts and comments tables.
+
+### Validating input
+
+By default all input is accepted and sent to the database. If you want to validate the input in a custom way,
+you may add the 'validation' middleware and define a 'validation.handler' function that returns a boolean
+indicating whether or not the value is valid.
+
+ 'validation.handler' => function ($operation, $tableName, $column, $value, $context) {
+ return ($column['name'] == 'post_id' && !is_numeric($value)) ? 'must be numeric' : true;
+ },
+
+When you edit a comment with id 4 using:
+
+ PUT /records/comments/4
+
+And you send as a body:
+
+ {"post_id":"two"}
+
+Then the server will return a '422' HTTP status code and nice error message:
+
+ {
+ "code": 1013,
+ "message": "Input validation failed for 'comments'",
+ "details": {
+ "post_id":"must be numeric"
+ }
+ }
+
+You can parse this output to make form fields show up with a red border and their appropriate error message.
+
+### Type validations
+
+If you enable the 'validation' middleware, then you (automatically) also enable type validation.
+This includes the following error messages:
+
+| error message | reason | applies to types |
+| ------------------- | --------------------------- | ------------------------------------------- |
+| cannot be null | unexpected null value | (any non-nullable column) |
+| illegal whitespace | leading/trailing whitespace | integer bigint decimal float double boolean |
+| invalid integer | illegal characters | integer bigint |
+| string too long | too many characters | varchar varbinary |
+| invalid decimal | illegal characters | decimal |
+| decimal too large | too many digits before dot | decimal |
+| decimal too precise | too many digits after dot | decimal |
+| invalid float | illegal characters | float double |
+| invalid boolean | use 1, 0, true or false | boolean |
+| invalid date | use yyyy-mm-dd | date |
+| invalid time | use hh:mm:ss | time |
+| invalid timestamp | use yyyy-mm-dd hh:mm:ss | timestamp |
+| invalid base64 | illegal characters | varbinary, blob |
+
+You may use the config settings "`validation.types`" and "`validation.tables`"' to define for which types and
+in which tables you want to apply type validation (defaults to 'all'). Example:
+
+ 'validation.types' => 'date,timestamp',
+ 'validation.tables' => 'posts,comments',
+
+Here we enable the type validation for date and timestamp fields in the posts and comments tables.
+
+NB: Types that are enabled will be checked for null values when the column is non-nullable.
+
+### Multi-tenancy support
+
+Two forms of multi-tenancy are supported:
+
+ - Single database, where every table has a tenant column (using the "multiTenancy" middleware).
+ - Multi database, where every tenant has it's own database (using the "reconnect" middleware).
+
+Below is an explanation of the corresponding middlewares.
+
+#### Multi-tenancy middleware
+
+You may use the "multiTenancy" middleware when you have a single multi-tenant database.
+If your tenants are identified by the "customer_id" column, then you can use the following handler:
+
+ 'multiTenancy.handler' => function ($operation, $tableName) {
+ return ['customer_id' => 12];
+ },
+
+This construct adds a filter requiring "customer_id" to be "12" to every operation (except for "create").
+It also sets the column "customer_id" on "create" to "12" and removes the column from any other write operation.
+
+NB: You may want to retrieve the customer id from the session (the "$_SESSION" variable).
+
+#### Reconnect middleware
+
+You may use the "reconnect" middleware when you have a separate database for each tenant.
+If the tenant has it's own database named "customer_12", then you can use the following handler:
+
+ 'reconnect.databaseHandler' => function () {
+ return 'customer_12';
+ },
+
+This will make the API reconnect to the database specifying "customer_12" as the database name. If you don't want
+to use the same credentials, then you should also implement the "usernameHandler" and "passwordHandler".
+
+NB: You may want to retrieve the database name from the session (the "$_SESSION" variable).
+
+### Prevent database scraping
+
+You may use the "joinLimits" and "pageLimits" middleware to prevent database scraping.
+The "joinLimits" middleware limits the table depth, number of tables and number of records returned in a join operation.
+If you want to allow 5 direct direct joins with a maximum of 25 records each, you can specify:
+
+ 'joinLimits.depth' => 1,
+ 'joinLimits.tables' => 5,
+ 'joinLimits.records' => 25,
+
+The "pageLimits" middleware limits the page number and the number records returned from a list operation.
+If you want to allow no more than 10 pages with a maximum of 25 records each, you can specify:
+
+ 'pageLimits.pages' => 10,
+ 'pageLimits.records' => 25,
+
+NB: The maximum number of records is also applied when there is no page number specified in the request.
+
+### Customization handlers
+
+You may use the "customization" middleware to modify request and response and implement any other functionality.
+
+ 'customization.beforeHandler' => function ($operation, $tableName, $request, $environment) {
+ $environment->start = microtime(true);
+ },
+ 'customization.afterHandler' => function ($operation, $tableName, $response, $environment) {
+ return $response->withHeader('X-Time-Taken', microtime(true) - $environment->start);
+ },
+
+The above example will add a header "X-Time-Taken" with the number of seconds the API call has taken.
+
+### XML middleware
+
+You may use the "xml" middleware to translate input and output from JSON to XML. This request:
+
+ GET /records/posts/1
+
+Outputs (when "pretty printed"):
+
+ {
+ "id": 1,
+ "user_id": 1,
+ "category_id": 1,
+ "content": "blog started"
+ }
+
+While (note the "format" query parameter):
+
+ GET /records/posts/1?format=xml
+
+Outputs:
+
+
+ 1
+ 1
+ 1
+ blog started
+
+
+This functionality is disabled by default and must be enabled using the "middlewares" configuration setting.
+
+### File uploads
+
+File uploads are supported through the [FileReader API](https://caniuse.com/#feat=filereader), check out the [example](https://github.com/mevdschee/php-crud-api/blob/master/examples/clients/upload/vanilla.html).
+
+## OpenAPI specification
+
+On the "/openapi" end-point the OpenAPI 3.0 (formerly called "Swagger") specification is served.
+It is a machine readable instant documentation of your API. To learn more, check out these links:
+
+- [Swagger Editor](https://editor.swagger.io/) can be used to view and debug the generated specification.
+- [OpenAPI specification](https://swagger.io/specification/) is a manual for creating an OpenAPI specification.
+- [Swagger Petstore](https://petstore.swagger.io/) is an example documentation that is generated using OpenAPI.
+
+## Cache
+
+There are 4 cache engines that can be configured by the "cacheType" config parameter:
+
+- TempFile (default)
+- Redis
+- Memcache
+- Memcached
+
+You can install the dependencies for the last three engines by running:
+
+ sudo apt install php-redis redis
+ sudo apt install php-memcache memcached
+ sudo apt install php-memcached memcached
+
+The default engine has no dependencies and will use temporary files in the system "temp" path.
+
+You may use the "cachePath" config parameter to specify the file system path for the temporary files or
+in case that you use a non-default "cacheType" the hostname (optionally with port) of the cache server.
+
+## Types
+
+These are the supported types with their length, category, JSON type and format:
+
+| type | length | category | JSON type | format |
+| ---------- | ------ | --------- | --------- | ------------------- |
+| varchar | 255 | character | string | |
+| clob | | character | string | |
+| boolean | | boolean | boolean | |
+| integer | | integer | number | |
+| bigint | | integer | number | |
+| float | | float | number | |
+| double | | float | number | |
+| decimal | 19,4 | decimal | string | |
+| date | | date/time | string | yyyy-mm-dd |
+| time | | date/time | string | hh:mm:ss |
+| timestamp | | date/time | string | yyyy-mm-dd hh:mm:ss |
+| varbinary | 255 | binary | string | base64 encoded |
+| blob | | binary | string | base64 encoded |
+| geometry | | other | string | well-known text |
+
+Note that geometry is a non-jdbc type and thus has limited support.
+
+## Data types in JavaScript
+
+Javascript and Javascript object notation (JSON) are not very well suited for reading database records. Decimal, date/time, binary and geometry types must be represented as strings in JSON (binary is base64 encoded, geometries are in WKT format). Below are two more serious issues described.
+
+### 64 bit integers
+
+JavaScript does not support 64 bit integers. All numbers are stored as 64 bit floating point values. The mantissa of a 64 bit floating point number is only 53 bit and that is why all integer numbers bigger than 53 bit may cause problems in JavaScript.
+
+### Inf and NaN floats
+
+The valid floating point values 'Infinite' (calculated with '1/0') and 'Not a Number' (calculated with '0/0') cannot be expressed in JSON, as they are not supported by the [JSON specification](https://www.json.org). When these values are stored in a database then you cannot read them as this script outputs database records as JSON.
+
+## Errors
+
+The following errors may be reported:
+
+| Error | HTTP response code | Message
+| ------| -------------------------- | --------------
+| 1000 | 404 Not found | Route not found
+| 1001 | 404 Not found | Table not found
+| 1002 | 422 Unprocessable entity | Argument count mismatch
+| 1003 | 404 Not found | Record not found
+| 1004 | 403 Forbidden | Origin is forbidden
+| 1005 | 404 Not found | Column not found
+| 1006 | 409 Conflict | Table already exists
+| 1007 | 409 Conflict | Column already exists
+| 1008 | 422 Unprocessable entity | Cannot read HTTP message
+| 1009 | 409 Conflict | Duplicate key exception
+| 1010 | 409 Conflict | Data integrity violation
+| 1011 | 401 Unauthorized | Authentication required
+| 1012 | 403 Forbidden | Authentication failed
+| 1013 | 422 Unprocessable entity | Input validation failed
+| 1014 | 403 Forbidden | Operation forbidden
+| 1015 | 405 Method not allowed | Operation not supported
+| 1016 | 403 Forbidden | Temporary or permanently blocked
+| 1017 | 403 Forbidden | Bad or missing XSRF token
+| 1018 | 403 Forbidden | Only AJAX requests allowed
+| 1019 | 403 Forbidden | Pagination Forbidden
+| 9999 | 500 Internal server error | Unknown error
+
+The following JSON structure is used:
+
+ {
+ "code":1002,
+ "message":"Argument count mismatch in '1'"
+ }
+
+NB: Any non-error response will have status: 200 OK
+
+## Tests
+
+I am testing mainly on Ubuntu and I have the following test setups:
+
+ - (Docker) Ubuntu 16.04 with PHP 7.0, MariaDB 10.0, PostgreSQL 9.5 (PostGIS 2.2) and SQL Server 2017
+ - (Docker) Debian 9 with PHP 7.0, MariaDB 10.1, PostgreSQL 9.6 (PostGIS 2.3) and SQLite 3.16
+ - (Docker) Ubuntu 18.04 with PHP 7.2, MySQL 5.7, PostgreSQL 10.4 (PostGIS 2.4) and SQLite 3.22
+ - (Docker) Debian 10 with PHP 7.3, MariaDB 10.3, PostgreSQL 11.4 (PostGIS 2.5) and SQLite 3.27
+ - (Docker) Ubuntu 20.04 with PHP 7.4, MySQL 8.0, PostgreSQL 12.2 (PostGIS 3.0) and SQLite 3.31
+ - (Docker) CentOS 8 with PHP 7.4, MariaDB 10.5, PostgreSQL 12.5 (PostGIS 3.0) and SQLite 3.26
+
+This covers not all environments (yet), so please notify me of failing tests and report your environment.
+I will try to cover most relevant setups in the "docker" folder of the project.
+
+### Running
+
+To run the functional tests locally you may run the following command:
+
+ php test.php
+
+This runs the functional tests from the "tests" directory. It uses the database dumps (fixtures) and
+database configuration (config) from the corresponding subdirectories.
+
+## Nginx config example
+```
+server {
+ listen 80 default_server;
+ listen [::]:80 default_server;
+
+ root /var/www/html;
+ index index.php index.html index.htm index.nginx-debian.html;
+ server_name server_domain_or_IP;
+
+ location / {
+ try_files $uri $uri/ =404;
+ }
+
+ location ~ [^/]\.php(/|$) {
+ fastcgi_split_path_info ^(.+\.php)(/.+)$;
+ try_files $fastcgi_script_name =404;
+ set $path_info $fastcgi_path_info;
+ fastcgi_param PATH_INFO $path_info;
+ fastcgi_index index.php;
+ include fastcgi.conf;
+ fastcgi_pass unix:/run/php/php7.0-fpm.sock;
+ }
+
+ location ~ /\.ht {
+ deny all;
+ }
+}
+```
+
+### Docker tests
+
+Install docker using the following commands and then logout and login for the changes to take effect:
+
+ sudo apt install docker.io
+ sudo usermod -aG docker ${USER}
+
+To run the docker tests run "build_all.sh" and "run_all.sh" from the docker directory. The output should be:
+
+ ================================================
+ CentOS 8 (PHP 7.4)
+ ================================================
+ [1/4] Starting MariaDB 10.5 ..... done
+ [2/4] Starting PostgreSQL 12.5 .. done
+ [3/4] Starting SQLServer 2017 ... skipped
+ [4/4] Cloning PHP-CRUD-API v2 ... skipped
+ ------------------------------------------------
+ mysql: 110 tests ran in 1911 ms, 1 skipped, 0 failed
+ pgsql: 110 tests ran in 1112 ms, 1 skipped, 0 failed
+ sqlsrv: skipped, driver not loaded
+ sqlite: 110 tests ran in 1178 ms, 12 skipped, 0 failed
+ ================================================
+ Debian 10 (PHP 7.3)
+ ================================================
+ [1/4] Starting MariaDB 10.3 ..... done
+ [2/4] Starting PostgreSQL 11.4 .. done
+ [3/4] Starting SQLServer 2017 ... skipped
+ [4/4] Cloning PHP-CRUD-API v2 ... skipped
+ ------------------------------------------------
+ mysql: 110 tests ran in 3459 ms, 1 skipped, 0 failed
+ pgsql: 110 tests ran in 1134 ms, 1 skipped, 0 failed
+ sqlsrv: skipped, driver not loaded
+ sqlite: 110 tests ran in 1275 ms, 12 skipped, 0 failed
+ ================================================
+ Debian 9 (PHP 7.0)
+ ================================================
+ [1/4] Starting MariaDB 10.1 ..... done
+ [2/4] Starting PostgreSQL 9.6 ... done
+ [3/4] Starting SQLServer 2017 ... skipped
+ [4/4] Cloning PHP-CRUD-API v2 ... skipped
+ ------------------------------------------------
+ mysql: 110 tests ran in 3181 ms, 1 skipped, 0 failed
+ pgsql: 110 tests ran in 1201 ms, 1 skipped, 0 failed
+ sqlsrv: skipped, driver not loaded
+ sqlite: 110 tests ran in 1414 ms, 12 skipped, 0 failed
+ ================================================
+ Ubuntu 16.04 (PHP 7.0)
+ ================================================
+ [1/4] Starting MariaDB 10.0 ..... done
+ [2/4] Starting PostgreSQL 9.5 ... done
+ [3/4] Starting SQLServer 2017 ... done
+ [4/4] Cloning PHP-CRUD-API v2 ... skipped
+ ------------------------------------------------
+ mysql: 110 tests ran in 3168 ms, 1 skipped, 0 failed
+ pgsql: 110 tests ran in 1197 ms, 1 skipped, 0 failed
+ sqlsrv: 110 tests ran in 10151 ms, 1 skipped, 0 failed
+ sqlite: skipped, driver not loaded
+ ================================================
+ Ubuntu 18.04 (PHP 7.2)
+ ================================================
+ [1/4] Starting MySQL 5.7 ........ done
+ [2/4] Starting PostgreSQL 10.4 .. done
+ [3/4] Starting SQLServer 2017 ... skipped
+ [4/4] Cloning PHP-CRUD-API v2 ... skipped
+ ------------------------------------------------
+ mysql: 110 tests ran in 3709 ms, 1 skipped, 0 failed
+ pgsql: 110 tests ran in 1334 ms, 1 skipped, 0 failed
+ sqlsrv: skipped, driver not loaded
+ sqlite: 110 tests ran in 1477 ms, 12 skipped, 0 failed
+ ================================================
+ Ubuntu 20.04 (PHP 7.4)
+ ================================================
+ [1/4] Starting MySQL 8.0 ........ done
+ [2/4] Starting PostgreSQL 12.2 .. done
+ [3/4] Starting SQLServer 2017 ... skipped
+ [4/4] Cloning PHP-CRUD-API v2 ... skipped
+ ------------------------------------------------
+ mysql: 110 tests ran in 5102 ms, 1 skipped, 0 failed
+ pgsql: 110 tests ran in 1170 ms, 1 skipped, 0 failed
+ sqlsrv: skipped, driver not loaded
+ sqlite: 110 tests ran in 1380 ms, 12 skipped, 0 failed
+
+The above test run (including starting up the databases) takes less than 5 minutes on my slow laptop.
+
+ $ ./run.sh
+ 1) centos8
+ 2) debian10
+ 3) debian9
+ 4) ubuntu16
+ 5) ubuntu18
+ 6) ubuntu20
+ > 5
+ ================================================
+ Ubuntu 18.04 (PHP 7.2)
+ ================================================
+ [1/4] Starting MySQL 5.7 ........ done
+ [2/4] Starting PostgreSQL 10.4 .. done
+ [3/4] Starting SQLServer 2017 ... skipped
+ [4/4] Cloning PHP-CRUD-API v2 ... skipped
+ ------------------------------------------------
+ mysql: 105 tests ran in 3390 ms, 1 skipped, 0 failed
+ pgsql: 105 tests ran in 936 ms, 1 skipped, 0 failed
+ sqlsrv: skipped, driver not loaded
+ sqlite: 105 tests ran in 1063 ms, 12 skipped, 0 failed
+ root@b7ab9472e08f:/php-crud-api#
+
+As you can see the "run.sh" script gives you access to a prompt in a chosen the docker environment.
+In this environment the local files are mounted. This allows for easy debugging on different environments.
+You may type "exit" when you are done.
+
+### Docker image
+
+There is a `Dockerfile` in the repository that is used to build an image at:
+
+[https://hub.docker.com/r/mevdschee/php-crud-api](https://hub.docker.com/r/mevdschee/php-crud-api)
+
+It will be automatically build on every release. The "latest" tag points to the last release.
+
+### Docker compose
+
+This repository also contains a `docker-compose.yml` file that you can install/build/run using:
+
+ sudo apt install docker-compose
+ docker-compose build
+ docker-compose up
+
+This will setup a database (MySQL) and a webserver (Apache) and runs the application using the blog example data used in the tests.
+
+Test the script (running in the container) by opening the following URL:
+
+ http://localhost:8080/records/posts/1
+
+Enjoy!
diff --git a/seatmap-webapi/api.include.php b/seatmap-webapi/api.include.php
new file mode 100644
index 0000000..20d074c
--- /dev/null
+++ b/seatmap-webapi/api.include.php
@@ -0,0 +1,11387 @@
+getHeaders() as $name => $values) {
+ * echo $name . ": " . implode(", ", $values);
+ * }
+ *
+ * // Emit headers iteratively:
+ * foreach ($message->getHeaders() as $name => $values) {
+ * foreach ($values as $value) {
+ * header(sprintf('%s: %s', $name, $value), false);
+ * }
+ * }
+ *
+ * While header names are not case-sensitive, getHeaders() will preserve the
+ * exact case in which headers were originally specified.
+ *
+ * @return string[][] Returns an associative array of the message's headers. Each
+ * key MUST be a header name, and each value MUST be an array of strings
+ * for that header.
+ */
+ public function getHeaders();
+
+ /**
+ * Checks if a header exists by the given case-insensitive name.
+ *
+ * @param string $name Case-insensitive header field name.
+ * @return bool Returns true if any header names match the given header
+ * name using a case-insensitive string comparison. Returns false if
+ * no matching header name is found in the message.
+ */
+ public function hasHeader($name);
+
+ /**
+ * Retrieves a message header value by the given case-insensitive name.
+ *
+ * This method returns an array of all the header values of the given
+ * case-insensitive header name.
+ *
+ * If the header does not appear in the message, this method MUST return an
+ * empty array.
+ *
+ * @param string $name Case-insensitive header field name.
+ * @return string[] An array of string values as provided for the given
+ * header. If the header does not appear in the message, this method MUST
+ * return an empty array.
+ */
+ public function getHeader($name);
+
+ /**
+ * Retrieves a comma-separated string of the values for a single header.
+ *
+ * This method returns all of the header values of the given
+ * case-insensitive header name as a string concatenated together using
+ * a comma.
+ *
+ * NOTE: Not all header values may be appropriately represented using
+ * comma concatenation. For such headers, use getHeader() instead
+ * and supply your own delimiter when concatenating.
+ *
+ * If the header does not appear in the message, this method MUST return
+ * an empty string.
+ *
+ * @param string $name Case-insensitive header field name.
+ * @return string A string of values as provided for the given header
+ * concatenated together using a comma. If the header does not appear in
+ * the message, this method MUST return an empty string.
+ */
+ public function getHeaderLine($name);
+
+ /**
+ * Return an instance with the provided value replacing the specified header.
+ *
+ * While header names are case-insensitive, the casing of the header will
+ * be preserved by this function, and returned from getHeaders().
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * new and/or updated header and value.
+ *
+ * @param string $name Case-insensitive header field name.
+ * @param string|string[] $value Header value(s).
+ * @return static
+ * @throws \InvalidArgumentException for invalid header names or values.
+ */
+ public function withHeader($name, $value);
+
+ /**
+ * Return an instance with the specified header appended with the given value.
+ *
+ * Existing values for the specified header will be maintained. The new
+ * value(s) will be appended to the existing list. If the header did not
+ * exist previously, it will be added.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * new header and/or value.
+ *
+ * @param string $name Case-insensitive header field name to add.
+ * @param string|string[] $value Header value(s).
+ * @return static
+ * @throws \InvalidArgumentException for invalid header names or values.
+ */
+ public function withAddedHeader($name, $value);
+
+ /**
+ * Return an instance without the specified header.
+ *
+ * Header resolution MUST be done without case-sensitivity.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that removes
+ * the named header.
+ *
+ * @param string $name Case-insensitive header field name to remove.
+ * @return static
+ */
+ public function withoutHeader($name);
+
+ /**
+ * Gets the body of the message.
+ *
+ * @return StreamInterface Returns the body as a stream.
+ */
+ public function getBody();
+
+ /**
+ * Return an instance with the specified message body.
+ *
+ * The body MUST be a StreamInterface object.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return a new instance that has the
+ * new body stream.
+ *
+ * @param StreamInterface $body Body.
+ * @return static
+ * @throws \InvalidArgumentException When the body is not valid.
+ */
+ public function withBody(StreamInterface $body);
+ }
+}
+
+// file: vendor/psr/http-message/src/RequestInterface.php
+namespace Psr\Http\Message {
+
+ /**
+ * Representation of an outgoing, client-side request.
+ *
+ * Per the HTTP specification, this interface includes properties for
+ * each of the following:
+ *
+ * - Protocol version
+ * - HTTP method
+ * - URI
+ * - Headers
+ * - Message body
+ *
+ * During construction, implementations MUST attempt to set the Host header from
+ * a provided URI if no Host header is provided.
+ *
+ * Requests are considered immutable; all methods that might change state MUST
+ * be implemented such that they retain the internal state of the current
+ * message and return an instance that contains the changed state.
+ */
+ interface RequestInterface extends MessageInterface
+ {
+ /**
+ * Retrieves the message's request target.
+ *
+ * Retrieves the message's request-target either as it will appear (for
+ * clients), as it appeared at request (for servers), or as it was
+ * specified for the instance (see withRequestTarget()).
+ *
+ * In most cases, this will be the origin-form of the composed URI,
+ * unless a value was provided to the concrete implementation (see
+ * withRequestTarget() below).
+ *
+ * If no URI is available, and no request-target has been specifically
+ * provided, this method MUST return the string "/".
+ *
+ * @return string
+ */
+ public function getRequestTarget();
+
+ /**
+ * Return an instance with the specific request-target.
+ *
+ * If the request needs a non-origin-form request-target — e.g., for
+ * specifying an absolute-form, authority-form, or asterisk-form —
+ * this method may be used to create an instance with the specified
+ * request-target, verbatim.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * changed request target.
+ *
+ * @link http://tools.ietf.org/html/rfc7230#section-5.3 (for the various
+ * request-target forms allowed in request messages)
+ * @param mixed $requestTarget
+ * @return static
+ */
+ public function withRequestTarget($requestTarget);
+
+ /**
+ * Retrieves the HTTP method of the request.
+ *
+ * @return string Returns the request method.
+ */
+ public function getMethod();
+
+ /**
+ * Return an instance with the provided HTTP method.
+ *
+ * While HTTP method names are typically all uppercase characters, HTTP
+ * method names are case-sensitive and thus implementations SHOULD NOT
+ * modify the given string.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * changed request method.
+ *
+ * @param string $method Case-sensitive method.
+ * @return static
+ * @throws \InvalidArgumentException for invalid HTTP methods.
+ */
+ public function withMethod($method);
+
+ /**
+ * Retrieves the URI instance.
+ *
+ * This method MUST return a UriInterface instance.
+ *
+ * @link http://tools.ietf.org/html/rfc3986#section-4.3
+ * @return UriInterface Returns a UriInterface instance
+ * representing the URI of the request.
+ */
+ public function getUri();
+
+ /**
+ * Returns an instance with the provided URI.
+ *
+ * This method MUST update the Host header of the returned request by
+ * default if the URI contains a host component. If the URI does not
+ * contain a host component, any pre-existing Host header MUST be carried
+ * over to the returned request.
+ *
+ * You can opt-in to preserving the original state of the Host header by
+ * setting `$preserveHost` to `true`. When `$preserveHost` is set to
+ * `true`, this method interacts with the Host header in the following ways:
+ *
+ * - If the Host header is missing or empty, and the new URI contains
+ * a host component, this method MUST update the Host header in the returned
+ * request.
+ * - If the Host header is missing or empty, and the new URI does not contain a
+ * host component, this method MUST NOT update the Host header in the returned
+ * request.
+ * - If a Host header is present and non-empty, this method MUST NOT update
+ * the Host header in the returned request.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * new UriInterface instance.
+ *
+ * @link http://tools.ietf.org/html/rfc3986#section-4.3
+ * @param UriInterface $uri New request URI to use.
+ * @param bool $preserveHost Preserve the original state of the Host header.
+ * @return static
+ */
+ public function withUri(UriInterface $uri, $preserveHost = false);
+ }
+}
+
+// file: vendor/psr/http-message/src/ResponseInterface.php
+namespace Psr\Http\Message {
+
+ /**
+ * Representation of an outgoing, server-side response.
+ *
+ * Per the HTTP specification, this interface includes properties for
+ * each of the following:
+ *
+ * - Protocol version
+ * - Status code and reason phrase
+ * - Headers
+ * - Message body
+ *
+ * Responses are considered immutable; all methods that might change state MUST
+ * be implemented such that they retain the internal state of the current
+ * message and return an instance that contains the changed state.
+ */
+ interface ResponseInterface extends MessageInterface
+ {
+ /**
+ * Gets the response status code.
+ *
+ * The status code is a 3-digit integer result code of the server's attempt
+ * to understand and satisfy the request.
+ *
+ * @return int Status code.
+ */
+ public function getStatusCode();
+
+ /**
+ * Return an instance with the specified status code and, optionally, reason phrase.
+ *
+ * If no reason phrase is specified, implementations MAY choose to default
+ * to the RFC 7231 or IANA recommended reason phrase for the response's
+ * status code.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated status and reason phrase.
+ *
+ * @link http://tools.ietf.org/html/rfc7231#section-6
+ * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+ * @param int $code The 3-digit integer result code to set.
+ * @param string $reasonPhrase The reason phrase to use with the
+ * provided status code; if none is provided, implementations MAY
+ * use the defaults as suggested in the HTTP specification.
+ * @return static
+ * @throws \InvalidArgumentException For invalid status code arguments.
+ */
+ public function withStatus($code, $reasonPhrase = '');
+
+ /**
+ * Gets the response reason phrase associated with the status code.
+ *
+ * Because a reason phrase is not a required element in a response
+ * status line, the reason phrase value MAY be null. Implementations MAY
+ * choose to return the default RFC 7231 recommended reason phrase (or those
+ * listed in the IANA HTTP Status Code Registry) for the response's
+ * status code.
+ *
+ * @link http://tools.ietf.org/html/rfc7231#section-6
+ * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+ * @return string Reason phrase; must return an empty string if none present.
+ */
+ public function getReasonPhrase();
+ }
+}
+
+// file: vendor/psr/http-message/src/ServerRequestInterface.php
+namespace Psr\Http\Message {
+
+ /**
+ * Representation of an incoming, server-side HTTP request.
+ *
+ * Per the HTTP specification, this interface includes properties for
+ * each of the following:
+ *
+ * - Protocol version
+ * - HTTP method
+ * - URI
+ * - Headers
+ * - Message body
+ *
+ * Additionally, it encapsulates all data as it has arrived to the
+ * application from the CGI and/or PHP environment, including:
+ *
+ * - The values represented in $_SERVER.
+ * - Any cookies provided (generally via $_COOKIE)
+ * - Query string arguments (generally via $_GET, or as parsed via parse_str())
+ * - Upload files, if any (as represented by $_FILES)
+ * - Deserialized body parameters (generally from $_POST)
+ *
+ * $_SERVER values MUST be treated as immutable, as they represent application
+ * state at the time of request; as such, no methods are provided to allow
+ * modification of those values. The other values provide such methods, as they
+ * can be restored from $_SERVER or the request body, and may need treatment
+ * during the application (e.g., body parameters may be deserialized based on
+ * content type).
+ *
+ * Additionally, this interface recognizes the utility of introspecting a
+ * request to derive and match additional parameters (e.g., via URI path
+ * matching, decrypting cookie values, deserializing non-form-encoded body
+ * content, matching authorization headers to users, etc). These parameters
+ * are stored in an "attributes" property.
+ *
+ * Requests are considered immutable; all methods that might change state MUST
+ * be implemented such that they retain the internal state of the current
+ * message and return an instance that contains the changed state.
+ */
+ interface ServerRequestInterface extends RequestInterface
+ {
+ /**
+ * Retrieve server parameters.
+ *
+ * Retrieves data related to the incoming request environment,
+ * typically derived from PHP's $_SERVER superglobal. The data IS NOT
+ * REQUIRED to originate from $_SERVER.
+ *
+ * @return array
+ */
+ public function getServerParams();
+
+ /**
+ * Retrieve cookies.
+ *
+ * Retrieves cookies sent by the client to the server.
+ *
+ * The data MUST be compatible with the structure of the $_COOKIE
+ * superglobal.
+ *
+ * @return array
+ */
+ public function getCookieParams();
+
+ /**
+ * Return an instance with the specified cookies.
+ *
+ * The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST
+ * be compatible with the structure of $_COOKIE. Typically, this data will
+ * be injected at instantiation.
+ *
+ * This method MUST NOT update the related Cookie header of the request
+ * instance, nor related values in the server params.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated cookie values.
+ *
+ * @param array $cookies Array of key/value pairs representing cookies.
+ * @return static
+ */
+ public function withCookieParams(array $cookies);
+
+ /**
+ * Retrieve query string arguments.
+ *
+ * Retrieves the deserialized query string arguments, if any.
+ *
+ * Note: the query params might not be in sync with the URI or server
+ * params. If you need to ensure you are only getting the original
+ * values, you may need to parse the query string from `getUri()->getQuery()`
+ * or from the `QUERY_STRING` server param.
+ *
+ * @return array
+ */
+ public function getQueryParams();
+
+ /**
+ * Return an instance with the specified query string arguments.
+ *
+ * These values SHOULD remain immutable over the course of the incoming
+ * request. They MAY be injected during instantiation, such as from PHP's
+ * $_GET superglobal, or MAY be derived from some other value such as the
+ * URI. In cases where the arguments are parsed from the URI, the data
+ * MUST be compatible with what PHP's parse_str() would return for
+ * purposes of how duplicate query parameters are handled, and how nested
+ * sets are handled.
+ *
+ * Setting query string arguments MUST NOT change the URI stored by the
+ * request, nor the values in the server params.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated query string arguments.
+ *
+ * @param array $query Array of query string arguments, typically from
+ * $_GET.
+ * @return static
+ */
+ public function withQueryParams(array $query);
+
+ /**
+ * Retrieve normalized file upload data.
+ *
+ * This method returns upload metadata in a normalized tree, with each leaf
+ * an instance of Psr\Http\Message\UploadedFileInterface.
+ *
+ * These values MAY be prepared from $_FILES or the message body during
+ * instantiation, or MAY be injected via withUploadedFiles().
+ *
+ * @return array An array tree of UploadedFileInterface instances; an empty
+ * array MUST be returned if no data is present.
+ */
+ public function getUploadedFiles();
+
+ /**
+ * Create a new instance with the specified uploaded files.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated body parameters.
+ *
+ * @param array $uploadedFiles An array tree of UploadedFileInterface instances.
+ * @return static
+ * @throws \InvalidArgumentException if an invalid structure is provided.
+ */
+ public function withUploadedFiles(array $uploadedFiles);
+
+ /**
+ * Retrieve any parameters provided in the request body.
+ *
+ * If the request Content-Type is either application/x-www-form-urlencoded
+ * or multipart/form-data, and the request method is POST, this method MUST
+ * return the contents of $_POST.
+ *
+ * Otherwise, this method may return any results of deserializing
+ * the request body content; as parsing returns structured content, the
+ * potential types MUST be arrays or objects only. A null value indicates
+ * the absence of body content.
+ *
+ * @return null|array|object The deserialized body parameters, if any.
+ * These will typically be an array or object.
+ */
+ public function getParsedBody();
+
+ /**
+ * Return an instance with the specified body parameters.
+ *
+ * These MAY be injected during instantiation.
+ *
+ * If the request Content-Type is either application/x-www-form-urlencoded
+ * or multipart/form-data, and the request method is POST, use this method
+ * ONLY to inject the contents of $_POST.
+ *
+ * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of
+ * deserializing the request body content. Deserialization/parsing returns
+ * structured data, and, as such, this method ONLY accepts arrays or objects,
+ * or a null value if nothing was available to parse.
+ *
+ * As an example, if content negotiation determines that the request data
+ * is a JSON payload, this method could be used to create a request
+ * instance with the deserialized parameters.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated body parameters.
+ *
+ * @param null|array|object $data The deserialized body data. This will
+ * typically be in an array or object.
+ * @return static
+ * @throws \InvalidArgumentException if an unsupported argument type is
+ * provided.
+ */
+ public function withParsedBody($data);
+
+ /**
+ * Retrieve attributes derived from the request.
+ *
+ * The request "attributes" may be used to allow injection of any
+ * parameters derived from the request: e.g., the results of path
+ * match operations; the results of decrypting cookies; the results of
+ * deserializing non-form-encoded message bodies; etc. Attributes
+ * will be application and request specific, and CAN be mutable.
+ *
+ * @return array Attributes derived from the request.
+ */
+ public function getAttributes();
+
+ /**
+ * Retrieve a single derived request attribute.
+ *
+ * Retrieves a single derived request attribute as described in
+ * getAttributes(). If the attribute has not been previously set, returns
+ * the default value as provided.
+ *
+ * This method obviates the need for a hasAttribute() method, as it allows
+ * specifying a default value to return if the attribute is not found.
+ *
+ * @see getAttributes()
+ * @param string $name The attribute name.
+ * @param mixed $default Default value to return if the attribute does not exist.
+ * @return mixed
+ */
+ public function getAttribute($name, $default = null);
+
+ /**
+ * Return an instance with the specified derived request attribute.
+ *
+ * This method allows setting a single derived request attribute as
+ * described in getAttributes().
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated attribute.
+ *
+ * @see getAttributes()
+ * @param string $name The attribute name.
+ * @param mixed $value The value of the attribute.
+ * @return static
+ */
+ public function withAttribute($name, $value);
+
+ /**
+ * Return an instance that removes the specified derived request attribute.
+ *
+ * This method allows removing a single derived request attribute as
+ * described in getAttributes().
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that removes
+ * the attribute.
+ *
+ * @see getAttributes()
+ * @param string $name The attribute name.
+ * @return static
+ */
+ public function withoutAttribute($name);
+ }
+}
+
+// file: vendor/psr/http-message/src/StreamInterface.php
+namespace Psr\Http\Message {
+
+ /**
+ * Describes a data stream.
+ *
+ * Typically, an instance will wrap a PHP stream; this interface provides
+ * a wrapper around the most common operations, including serialization of
+ * the entire stream to a string.
+ */
+ interface StreamInterface
+ {
+ /**
+ * Reads all data from the stream into a string, from the beginning to end.
+ *
+ * This method MUST attempt to seek to the beginning of the stream before
+ * reading data and read the stream until the end is reached.
+ *
+ * Warning: This could attempt to load a large amount of data into memory.
+ *
+ * This method MUST NOT raise an exception in order to conform with PHP's
+ * string casting operations.
+ *
+ * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
+ * @return string
+ */
+ public function __toString();
+
+ /**
+ * Closes the stream and any underlying resources.
+ *
+ * @return void
+ */
+ public function close();
+
+ /**
+ * Separates any underlying resources from the stream.
+ *
+ * After the stream has been detached, the stream is in an unusable state.
+ *
+ * @return resource|null Underlying PHP stream, if any
+ */
+ public function detach();
+
+ /**
+ * Get the size of the stream if known.
+ *
+ * @return int|null Returns the size in bytes if known, or null if unknown.
+ */
+ public function getSize();
+
+ /**
+ * Returns the current position of the file read/write pointer
+ *
+ * @return int Position of the file pointer
+ * @throws \RuntimeException on error.
+ */
+ public function tell();
+
+ /**
+ * Returns true if the stream is at the end of the stream.
+ *
+ * @return bool
+ */
+ public function eof();
+
+ /**
+ * Returns whether or not the stream is seekable.
+ *
+ * @return bool
+ */
+ public function isSeekable();
+
+ /**
+ * Seek to a position in the stream.
+ *
+ * @link http://www.php.net/manual/en/function.fseek.php
+ * @param int $offset Stream offset
+ * @param int $whence Specifies how the cursor position will be calculated
+ * based on the seek offset. Valid values are identical to the built-in
+ * PHP $whence values for `fseek()`. SEEK_SET: Set position equal to
+ * offset bytes SEEK_CUR: Set position to current location plus offset
+ * SEEK_END: Set position to end-of-stream plus offset.
+ * @throws \RuntimeException on failure.
+ */
+ public function seek($offset, $whence = SEEK_SET);
+
+ /**
+ * Seek to the beginning of the stream.
+ *
+ * If the stream is not seekable, this method will raise an exception;
+ * otherwise, it will perform a seek(0).
+ *
+ * @see seek()
+ * @link http://www.php.net/manual/en/function.fseek.php
+ * @throws \RuntimeException on failure.
+ */
+ public function rewind();
+
+ /**
+ * Returns whether or not the stream is writable.
+ *
+ * @return bool
+ */
+ public function isWritable();
+
+ /**
+ * Write data to the stream.
+ *
+ * @param string $string The string that is to be written.
+ * @return int Returns the number of bytes written to the stream.
+ * @throws \RuntimeException on failure.
+ */
+ public function write($string);
+
+ /**
+ * Returns whether or not the stream is readable.
+ *
+ * @return bool
+ */
+ public function isReadable();
+
+ /**
+ * Read data from the stream.
+ *
+ * @param int $length Read up to $length bytes from the object and return
+ * them. Fewer than $length bytes may be returned if underlying stream
+ * call returns fewer bytes.
+ * @return string Returns the data read from the stream, or an empty string
+ * if no bytes are available.
+ * @throws \RuntimeException if an error occurs.
+ */
+ public function read($length);
+
+ /**
+ * Returns the remaining contents in a string
+ *
+ * @return string
+ * @throws \RuntimeException if unable to read or an error occurs while
+ * reading.
+ */
+ public function getContents();
+
+ /**
+ * Get stream metadata as an associative array or retrieve a specific key.
+ *
+ * The keys returned are identical to the keys returned from PHP's
+ * stream_get_meta_data() function.
+ *
+ * @link http://php.net/manual/en/function.stream-get-meta-data.php
+ * @param string $key Specific metadata to retrieve.
+ * @return array|mixed|null Returns an associative array if no key is
+ * provided. Returns a specific key value if a key is provided and the
+ * value is found, or null if the key is not found.
+ */
+ public function getMetadata($key = null);
+ }
+}
+
+// file: vendor/psr/http-message/src/UploadedFileInterface.php
+namespace Psr\Http\Message {
+
+ /**
+ * Value object representing a file uploaded through an HTTP request.
+ *
+ * Instances of this interface are considered immutable; all methods that
+ * might change state MUST be implemented such that they retain the internal
+ * state of the current instance and return an instance that contains the
+ * changed state.
+ */
+ interface UploadedFileInterface
+ {
+ /**
+ * Retrieve a stream representing the uploaded file.
+ *
+ * This method MUST return a StreamInterface instance, representing the
+ * uploaded file. The purpose of this method is to allow utilizing native PHP
+ * stream functionality to manipulate the file upload, such as
+ * stream_copy_to_stream() (though the result will need to be decorated in a
+ * native PHP stream wrapper to work with such functions).
+ *
+ * If the moveTo() method has been called previously, this method MUST raise
+ * an exception.
+ *
+ * @return StreamInterface Stream representation of the uploaded file.
+ * @throws \RuntimeException in cases when no stream is available or can be
+ * created.
+ */
+ public function getStream();
+
+ /**
+ * Move the uploaded file to a new location.
+ *
+ * Use this method as an alternative to move_uploaded_file(). This method is
+ * guaranteed to work in both SAPI and non-SAPI environments.
+ * Implementations must determine which environment they are in, and use the
+ * appropriate method (move_uploaded_file(), rename(), or a stream
+ * operation) to perform the operation.
+ *
+ * $targetPath may be an absolute path, or a relative path. If it is a
+ * relative path, resolution should be the same as used by PHP's rename()
+ * function.
+ *
+ * The original file or stream MUST be removed on completion.
+ *
+ * If this method is called more than once, any subsequent calls MUST raise
+ * an exception.
+ *
+ * When used in an SAPI environment where $_FILES is populated, when writing
+ * files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be
+ * used to ensure permissions and upload status are verified correctly.
+ *
+ * If you wish to move to a stream, use getStream(), as SAPI operations
+ * cannot guarantee writing to stream destinations.
+ *
+ * @see http://php.net/is_uploaded_file
+ * @see http://php.net/move_uploaded_file
+ * @param string $targetPath Path to which to move the uploaded file.
+ * @throws \InvalidArgumentException if the $targetPath specified is invalid.
+ * @throws \RuntimeException on any error during the move operation, or on
+ * the second or subsequent call to the method.
+ */
+ public function moveTo($targetPath);
+
+ /**
+ * Retrieve the file size.
+ *
+ * Implementations SHOULD return the value stored in the "size" key of
+ * the file in the $_FILES array if available, as PHP calculates this based
+ * on the actual size transmitted.
+ *
+ * @return int|null The file size in bytes or null if unknown.
+ */
+ public function getSize();
+
+ /**
+ * Retrieve the error associated with the uploaded file.
+ *
+ * The return value MUST be one of PHP's UPLOAD_ERR_XXX constants.
+ *
+ * If the file was uploaded successfully, this method MUST return
+ * UPLOAD_ERR_OK.
+ *
+ * Implementations SHOULD return the value stored in the "error" key of
+ * the file in the $_FILES array.
+ *
+ * @see http://php.net/manual/en/features.file-upload.errors.php
+ * @return int One of PHP's UPLOAD_ERR_XXX constants.
+ */
+ public function getError();
+
+ /**
+ * Retrieve the filename sent by the client.
+ *
+ * Do not trust the value returned by this method. A client could send
+ * a malicious filename with the intention to corrupt or hack your
+ * application.
+ *
+ * Implementations SHOULD return the value stored in the "name" key of
+ * the file in the $_FILES array.
+ *
+ * @return string|null The filename sent by the client or null if none
+ * was provided.
+ */
+ public function getClientFilename();
+
+ /**
+ * Retrieve the media type sent by the client.
+ *
+ * Do not trust the value returned by this method. A client could send
+ * a malicious media type with the intention to corrupt or hack your
+ * application.
+ *
+ * Implementations SHOULD return the value stored in the "type" key of
+ * the file in the $_FILES array.
+ *
+ * @return string|null The media type sent by the client or null if none
+ * was provided.
+ */
+ public function getClientMediaType();
+ }
+}
+
+// file: vendor/psr/http-message/src/UriInterface.php
+namespace Psr\Http\Message {
+
+ /**
+ * Value object representing a URI.
+ *
+ * This interface is meant to represent URIs according to RFC 3986 and to
+ * provide methods for most common operations. Additional functionality for
+ * working with URIs can be provided on top of the interface or externally.
+ * Its primary use is for HTTP requests, but may also be used in other
+ * contexts.
+ *
+ * Instances of this interface are considered immutable; all methods that
+ * might change state MUST be implemented such that they retain the internal
+ * state of the current instance and return an instance that contains the
+ * changed state.
+ *
+ * Typically the Host header will be also be present in the request message.
+ * For server-side requests, the scheme will typically be discoverable in the
+ * server parameters.
+ *
+ * @link http://tools.ietf.org/html/rfc3986 (the URI specification)
+ */
+ interface UriInterface
+ {
+ /**
+ * Retrieve the scheme component of the URI.
+ *
+ * If no scheme is present, this method MUST return an empty string.
+ *
+ * The value returned MUST be normalized to lowercase, per RFC 3986
+ * Section 3.1.
+ *
+ * The trailing ":" character is not part of the scheme and MUST NOT be
+ * added.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-3.1
+ * @return string The URI scheme.
+ */
+ public function getScheme();
+
+ /**
+ * Retrieve the authority component of the URI.
+ *
+ * If no authority information is present, this method MUST return an empty
+ * string.
+ *
+ * The authority syntax of the URI is:
+ *
+ *
+ * [user-info@]host[:port]
+ *
+ *
+ * If the port component is not set or is the standard port for the current
+ * scheme, it SHOULD NOT be included.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-3.2
+ * @return string The URI authority, in "[user-info@]host[:port]" format.
+ */
+ public function getAuthority();
+
+ /**
+ * Retrieve the user information component of the URI.
+ *
+ * If no user information is present, this method MUST return an empty
+ * string.
+ *
+ * If a user is present in the URI, this will return that value;
+ * additionally, if the password is also present, it will be appended to the
+ * user value, with a colon (":") separating the values.
+ *
+ * The trailing "@" character is not part of the user information and MUST
+ * NOT be added.
+ *
+ * @return string The URI user information, in "username[:password]" format.
+ */
+ public function getUserInfo();
+
+ /**
+ * Retrieve the host component of the URI.
+ *
+ * If no host is present, this method MUST return an empty string.
+ *
+ * The value returned MUST be normalized to lowercase, per RFC 3986
+ * Section 3.2.2.
+ *
+ * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
+ * @return string The URI host.
+ */
+ public function getHost();
+
+ /**
+ * Retrieve the port component of the URI.
+ *
+ * If a port is present, and it is non-standard for the current scheme,
+ * this method MUST return it as an integer. If the port is the standard port
+ * used with the current scheme, this method SHOULD return null.
+ *
+ * If no port is present, and no scheme is present, this method MUST return
+ * a null value.
+ *
+ * If no port is present, but a scheme is present, this method MAY return
+ * the standard port for that scheme, but SHOULD return null.
+ *
+ * @return null|int The URI port.
+ */
+ public function getPort();
+
+ /**
+ * Retrieve the path component of the URI.
+ *
+ * The path can either be empty or absolute (starting with a slash) or
+ * rootless (not starting with a slash). Implementations MUST support all
+ * three syntaxes.
+ *
+ * Normally, the empty path "" and absolute path "/" are considered equal as
+ * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
+ * do this normalization because in contexts with a trimmed base path, e.g.
+ * the front controller, this difference becomes significant. It's the task
+ * of the user to handle both "" and "/".
+ *
+ * The value returned MUST be percent-encoded, but MUST NOT double-encode
+ * any characters. To determine what characters to encode, please refer to
+ * RFC 3986, Sections 2 and 3.3.
+ *
+ * As an example, if the value should include a slash ("/") not intended as
+ * delimiter between path segments, that value MUST be passed in encoded
+ * form (e.g., "%2F") to the instance.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-2
+ * @see https://tools.ietf.org/html/rfc3986#section-3.3
+ * @return string The URI path.
+ */
+ public function getPath();
+
+ /**
+ * Retrieve the query string of the URI.
+ *
+ * If no query string is present, this method MUST return an empty string.
+ *
+ * The leading "?" character is not part of the query and MUST NOT be
+ * added.
+ *
+ * The value returned MUST be percent-encoded, but MUST NOT double-encode
+ * any characters. To determine what characters to encode, please refer to
+ * RFC 3986, Sections 2 and 3.4.
+ *
+ * As an example, if a value in a key/value pair of the query string should
+ * include an ampersand ("&") not intended as a delimiter between values,
+ * that value MUST be passed in encoded form (e.g., "%26") to the instance.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-2
+ * @see https://tools.ietf.org/html/rfc3986#section-3.4
+ * @return string The URI query string.
+ */
+ public function getQuery();
+
+ /**
+ * Retrieve the fragment component of the URI.
+ *
+ * If no fragment is present, this method MUST return an empty string.
+ *
+ * The leading "#" character is not part of the fragment and MUST NOT be
+ * added.
+ *
+ * The value returned MUST be percent-encoded, but MUST NOT double-encode
+ * any characters. To determine what characters to encode, please refer to
+ * RFC 3986, Sections 2 and 3.5.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-2
+ * @see https://tools.ietf.org/html/rfc3986#section-3.5
+ * @return string The URI fragment.
+ */
+ public function getFragment();
+
+ /**
+ * Return an instance with the specified scheme.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified scheme.
+ *
+ * Implementations MUST support the schemes "http" and "https" case
+ * insensitively, and MAY accommodate other schemes if required.
+ *
+ * An empty scheme is equivalent to removing the scheme.
+ *
+ * @param string $scheme The scheme to use with the new instance.
+ * @return static A new instance with the specified scheme.
+ * @throws \InvalidArgumentException for invalid or unsupported schemes.
+ */
+ public function withScheme($scheme);
+
+ /**
+ * Return an instance with the specified user information.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified user information.
+ *
+ * Password is optional, but the user information MUST include the
+ * user; an empty string for the user is equivalent to removing user
+ * information.
+ *
+ * @param string $user The user name to use for authority.
+ * @param null|string $password The password associated with $user.
+ * @return static A new instance with the specified user information.
+ */
+ public function withUserInfo($user, $password = null);
+
+ /**
+ * Return an instance with the specified host.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified host.
+ *
+ * An empty host value is equivalent to removing the host.
+ *
+ * @param string $host The hostname to use with the new instance.
+ * @return static A new instance with the specified host.
+ * @throws \InvalidArgumentException for invalid hostnames.
+ */
+ public function withHost($host);
+
+ /**
+ * Return an instance with the specified port.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified port.
+ *
+ * Implementations MUST raise an exception for ports outside the
+ * established TCP and UDP port ranges.
+ *
+ * A null value provided for the port is equivalent to removing the port
+ * information.
+ *
+ * @param null|int $port The port to use with the new instance; a null value
+ * removes the port information.
+ * @return static A new instance with the specified port.
+ * @throws \InvalidArgumentException for invalid ports.
+ */
+ public function withPort($port);
+
+ /**
+ * Return an instance with the specified path.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified path.
+ *
+ * The path can either be empty or absolute (starting with a slash) or
+ * rootless (not starting with a slash). Implementations MUST support all
+ * three syntaxes.
+ *
+ * If the path is intended to be domain-relative rather than path relative then
+ * it must begin with a slash ("/"). Paths not starting with a slash ("/")
+ * are assumed to be relative to some base path known to the application or
+ * consumer.
+ *
+ * Users can provide both encoded and decoded path characters.
+ * Implementations ensure the correct encoding as outlined in getPath().
+ *
+ * @param string $path The path to use with the new instance.
+ * @return static A new instance with the specified path.
+ * @throws \InvalidArgumentException for invalid paths.
+ */
+ public function withPath($path);
+
+ /**
+ * Return an instance with the specified query string.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified query string.
+ *
+ * Users can provide both encoded and decoded query characters.
+ * Implementations ensure the correct encoding as outlined in getQuery().
+ *
+ * An empty query string value is equivalent to removing the query string.
+ *
+ * @param string $query The query string to use with the new instance.
+ * @return static A new instance with the specified query string.
+ * @throws \InvalidArgumentException for invalid query strings.
+ */
+ public function withQuery($query);
+
+ /**
+ * Return an instance with the specified URI fragment.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified URI fragment.
+ *
+ * Users can provide both encoded and decoded fragment characters.
+ * Implementations ensure the correct encoding as outlined in getFragment().
+ *
+ * An empty fragment value is equivalent to removing the fragment.
+ *
+ * @param string $fragment The fragment to use with the new instance.
+ * @return static A new instance with the specified fragment.
+ */
+ public function withFragment($fragment);
+
+ /**
+ * Return the string representation as a URI reference.
+ *
+ * Depending on which components of the URI are present, the resulting
+ * string is either a full URI or relative reference according to RFC 3986,
+ * Section 4.1. The method concatenates the various components of the URI,
+ * using the appropriate delimiters:
+ *
+ * - If a scheme is present, it MUST be suffixed by ":".
+ * - If an authority is present, it MUST be prefixed by "//".
+ * - The path can be concatenated without delimiters. But there are two
+ * cases where the path has to be adjusted to make the URI reference
+ * valid as PHP does not allow to throw an exception in __toString():
+ * - If the path is rootless and an authority is present, the path MUST
+ * be prefixed by "/".
+ * - If the path is starting with more than one "/" and no authority is
+ * present, the starting slashes MUST be reduced to one.
+ * - If a query is present, it MUST be prefixed by "?".
+ * - If a fragment is present, it MUST be prefixed by "#".
+ *
+ * @see http://tools.ietf.org/html/rfc3986#section-4.1
+ * @return string
+ */
+ public function __toString();
+ }
+}
+
+// file: vendor/psr/http-server-handler/src/RequestHandlerInterface.php
+namespace Psr\Http\Server {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+
+ /**
+ * Handles a server request and produces a response.
+ *
+ * An HTTP request handler process an HTTP request in order to produce an
+ * HTTP response.
+ */
+ interface RequestHandlerInterface
+ {
+ /**
+ * Handles a request and produces a response.
+ *
+ * May call other collaborating code to generate the response.
+ */
+ public function handle(ServerRequestInterface $request): ResponseInterface;
+ }
+}
+
+// file: vendor/psr/http-server-middleware/src/MiddlewareInterface.php
+namespace Psr\Http\Server {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+
+ /**
+ * Participant in processing a server request and response.
+ *
+ * An HTTP middleware component participates in processing an HTTP message:
+ * by acting on the request, generating the response, or forwarding the
+ * request to a subsequent middleware and possibly acting on its response.
+ */
+ interface MiddlewareInterface
+ {
+ /**
+ * Process an incoming server request.
+ *
+ * Processes an incoming server request in order to produce a response.
+ * If unable to produce the response itself, it may delegate to the provided
+ * request handler to do so.
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface;
+ }
+}
+
+// file: vendor/nyholm/psr7/src/Factory/Psr17Factory.php
+namespace Nyholm\Psr7\Factory {
+
+ use Nyholm\Psr7\{Request, Response, ServerRequest, Stream, UploadedFile, Uri};
+ use Psr\Http\Message\{RequestFactoryInterface, RequestInterface, ResponseFactoryInterface, ResponseInterface, ServerRequestFactoryInterface, ServerRequestInterface, StreamFactoryInterface, StreamInterface, UploadedFileFactoryInterface, UploadedFileInterface, UriFactoryInterface, UriInterface};
+
+ /**
+ * @author Tobias Nyholm
+ * @author Martijn van der Ven
+ */
+ final class Psr17Factory implements RequestFactoryInterface, ResponseFactoryInterface, ServerRequestFactoryInterface, StreamFactoryInterface, UploadedFileFactoryInterface, UriFactoryInterface
+ {
+ public function createRequest(string $method, $uri): RequestInterface
+ {
+ return new Request($method, $uri);
+ }
+
+ public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface
+ {
+ if (2 > \func_num_args()) {
+ // This will make the Response class to use a custom reasonPhrase
+ $reasonPhrase = null;
+ }
+
+ return new Response($code, [], null, '1.1', $reasonPhrase);
+ }
+
+ public function createStream(string $content = ''): StreamInterface
+ {
+ return Stream::create($content);
+ }
+
+ public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface
+ {
+ $resource = @\fopen($filename, $mode);
+ if (false === $resource) {
+ if ('' === $mode || false === \in_array($mode[0], ['r', 'w', 'a', 'x', 'c'])) {
+ throw new \InvalidArgumentException('The mode ' . $mode . ' is invalid.');
+ }
+
+ throw new \RuntimeException('The file ' . $filename . ' cannot be opened.');
+ }
+
+ return Stream::create($resource);
+ }
+
+ public function createStreamFromResource($resource): StreamInterface
+ {
+ return Stream::create($resource);
+ }
+
+ public function createUploadedFile(StreamInterface $stream, int $size = null, int $error = \UPLOAD_ERR_OK, string $clientFilename = null, string $clientMediaType = null): UploadedFileInterface
+ {
+ if (null === $size) {
+ $size = $stream->getSize();
+ }
+
+ return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType);
+ }
+
+ public function createUri(string $uri = ''): UriInterface
+ {
+ return new Uri($uri);
+ }
+
+ public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
+ {
+ return new ServerRequest($method, $uri, [], null, '1.1', $serverParams);
+ }
+ }
+}
+
+// file: vendor/nyholm/psr7/src/MessageTrait.php
+namespace Nyholm\Psr7 {
+
+ use Psr\Http\Message\StreamInterface;
+
+ /**
+ * Trait implementing functionality common to requests and responses.
+ *
+ * @author Michael Dowling and contributors to guzzlehttp/psr7
+ * @author Tobias Nyholm
+ * @author Martijn van der Ven
+ *
+ * @internal should not be used outside of Nyholm/Psr7 as it does not fall under our BC promise
+ */
+ trait MessageTrait
+ {
+ /** @var array Map of all registered headers, as original name => array of values */
+ private $headers = [];
+
+ /** @var array Map of lowercase header name => original name at registration */
+ private $headerNames = [];
+
+ /** @var string */
+ private $protocol = '1.1';
+
+ /** @var StreamInterface|null */
+ private $stream;
+
+ public function getProtocolVersion(): string
+ {
+ return $this->protocol;
+ }
+
+ public function withProtocolVersion($version): self
+ {
+ if ($this->protocol === $version) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->protocol = $version;
+
+ return $new;
+ }
+
+ public function getHeaders(): array
+ {
+ return $this->headers;
+ }
+
+ public function hasHeader($header): bool
+ {
+ return isset($this->headerNames[\strtolower($header)]);
+ }
+
+ public function getHeader($header): array
+ {
+ $header = \strtolower($header);
+ if (!isset($this->headerNames[$header])) {
+ return [];
+ }
+
+ $header = $this->headerNames[$header];
+
+ return $this->headers[$header];
+ }
+
+ public function getHeaderLine($header): string
+ {
+ return \implode(', ', $this->getHeader($header));
+ }
+
+ public function withHeader($header, $value): self
+ {
+ $value = $this->validateAndTrimHeader($header, $value);
+ $normalized = \strtolower($header);
+
+ $new = clone $this;
+ if (isset($new->headerNames[$normalized])) {
+ unset($new->headers[$new->headerNames[$normalized]]);
+ }
+ $new->headerNames[$normalized] = $header;
+ $new->headers[$header] = $value;
+
+ return $new;
+ }
+
+ public function withAddedHeader($header, $value): self
+ {
+ if (!\is_string($header) || '' === $header) {
+ throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string.');
+ }
+
+ $new = clone $this;
+ $new->setHeaders([$header => $value]);
+
+ return $new;
+ }
+
+ public function withoutHeader($header): self
+ {
+ $normalized = \strtolower($header);
+ if (!isset($this->headerNames[$normalized])) {
+ return $this;
+ }
+
+ $header = $this->headerNames[$normalized];
+ $new = clone $this;
+ unset($new->headers[$header], $new->headerNames[$normalized]);
+
+ return $new;
+ }
+
+ public function getBody(): StreamInterface
+ {
+ if (null === $this->stream) {
+ $this->stream = Stream::create('');
+ }
+
+ return $this->stream;
+ }
+
+ public function withBody(StreamInterface $body): self
+ {
+ if ($body === $this->stream) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->stream = $body;
+
+ return $new;
+ }
+
+ private function setHeaders(array $headers) /*:void*/
+ {
+ foreach ($headers as $header => $value) {
+ $value = $this->validateAndTrimHeader($header, $value);
+ $normalized = \strtolower($header);
+ if (isset($this->headerNames[$normalized])) {
+ $header = $this->headerNames[$normalized];
+ $this->headers[$header] = \array_merge($this->headers[$header], $value);
+ } else {
+ $this->headerNames[$normalized] = $header;
+ $this->headers[$header] = $value;
+ }
+ }
+ }
+
+ /**
+ * Make sure the header complies with RFC 7230.
+ *
+ * Header names must be a non-empty string consisting of token characters.
+ *
+ * Header values must be strings consisting of visible characters with all optional
+ * leading and trailing whitespace stripped. This method will always strip such
+ * optional whitespace. Note that the method does not allow folding whitespace within
+ * the values as this was deprecated for almost all instances by the RFC.
+ *
+ * header-field = field-name ":" OWS field-value OWS
+ * field-name = 1*( "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^"
+ * / "_" / "`" / "|" / "~" / %x30-39 / ( %x41-5A / %x61-7A ) )
+ * OWS = *( SP / HTAB )
+ * field-value = *( ( %x21-7E / %x80-FF ) [ 1*( SP / HTAB ) ( %x21-7E / %x80-FF ) ] )
+ *
+ * @see https://tools.ietf.org/html/rfc7230#section-3.2.4
+ */
+ private function validateAndTrimHeader($header, $values): array
+ {
+ if (!\is_string($header) || 1 !== \preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@", $header)) {
+ throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string.');
+ }
+
+ if (!\is_array($values)) {
+ // This is simple, just one value.
+ if ((!\is_numeric($values) && !\is_string($values)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $values)) {
+ throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings.');
+ }
+
+ return [\trim((string) $values, " \t")];
+ }
+
+ if (empty($values)) {
+ throw new \InvalidArgumentException('Header values must be a string or an array of strings, empty array given.');
+ }
+
+ // Assert Non empty array
+ $returnValues = [];
+ foreach ($values as $v) {
+ if ((!\is_numeric($v) && !\is_string($v)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $v)) {
+ throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings.');
+ }
+
+ $returnValues[] = \trim((string) $v, " \t");
+ }
+
+ return $returnValues;
+ }
+ }
+}
+
+// file: vendor/nyholm/psr7/src/Request.php
+namespace Nyholm\Psr7 {
+
+ use Psr\Http\Message\{RequestInterface, StreamInterface, UriInterface};
+
+ /**
+ * @author Tobias Nyholm
+ * @author Martijn van der Ven
+ */
+ final class Request implements RequestInterface
+ {
+ use MessageTrait;
+ use RequestTrait;
+
+ /**
+ * @param string $method HTTP method
+ * @param string|UriInterface $uri URI
+ * @param array $headers Request headers
+ * @param string|resource|StreamInterface|null $body Request body
+ * @param string $version Protocol version
+ */
+ public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1')
+ {
+ if (!($uri instanceof UriInterface)) {
+ $uri = new Uri($uri);
+ }
+
+ $this->method = $method;
+ $this->uri = $uri;
+ $this->setHeaders($headers);
+ $this->protocol = $version;
+
+ if (!$this->hasHeader('Host')) {
+ $this->updateHostFromUri();
+ }
+
+ // If we got no body, defer initialization of the stream until Request::getBody()
+ if ('' !== $body && null !== $body) {
+ $this->stream = Stream::create($body);
+ }
+ }
+ }
+}
+
+// file: vendor/nyholm/psr7/src/RequestTrait.php
+namespace Nyholm\Psr7 {
+
+ use Psr\Http\Message\UriInterface;
+
+ /**
+ * @author Michael Dowling and contributors to guzzlehttp/psr7
+ * @author Tobias Nyholm
+ * @author Martijn van der Ven
+ *
+ * @internal should not be used outside of Nyholm/Psr7 as it does not fall under our BC promise
+ */
+ trait RequestTrait
+ {
+ /** @var string */
+ private $method;
+
+ /** @var string|null */
+ private $requestTarget;
+
+ /** @var UriInterface|null */
+ private $uri;
+
+ public function getRequestTarget(): string
+ {
+ if (null !== $this->requestTarget) {
+ return $this->requestTarget;
+ }
+
+ if ('' === $target = $this->uri->getPath()) {
+ $target = '/';
+ }
+ if ('' !== $this->uri->getQuery()) {
+ $target .= '?' . $this->uri->getQuery();
+ }
+
+ return $target;
+ }
+
+ public function withRequestTarget($requestTarget): self
+ {
+ if (\preg_match('#\s#', $requestTarget)) {
+ throw new \InvalidArgumentException('Invalid request target provided; cannot contain whitespace');
+ }
+
+ $new = clone $this;
+ $new->requestTarget = $requestTarget;
+
+ return $new;
+ }
+
+ public function getMethod(): string
+ {
+ return $this->method;
+ }
+
+ public function withMethod($method): self
+ {
+ if (!\is_string($method)) {
+ throw new \InvalidArgumentException('Method must be a string');
+ }
+
+ $new = clone $this;
+ $new->method = $method;
+
+ return $new;
+ }
+
+ public function getUri(): UriInterface
+ {
+ return $this->uri;
+ }
+
+ public function withUri(UriInterface $uri, $preserveHost = false): self
+ {
+ if ($uri === $this->uri) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->uri = $uri;
+
+ if (!$preserveHost || !$this->hasHeader('Host')) {
+ $new->updateHostFromUri();
+ }
+
+ return $new;
+ }
+
+ private function updateHostFromUri() /*:void*/
+ {
+ if ('' === $host = $this->uri->getHost()) {
+ return;
+ }
+
+ if (null !== ($port = $this->uri->getPort())) {
+ $host .= ':' . $port;
+ }
+
+ if (isset($this->headerNames['host'])) {
+ $header = $this->headerNames['host'];
+ } else {
+ $this->headerNames['host'] = $header = 'Host';
+ }
+
+ // Ensure Host is the first header.
+ // See: http://tools.ietf.org/html/rfc7230#section-5.4
+ $this->headers = [$header => [$host]] + $this->headers;
+ }
+ }
+}
+
+// file: vendor/nyholm/psr7/src/Response.php
+namespace Nyholm\Psr7 {
+
+ use Psr\Http\Message\{ResponseInterface, StreamInterface};
+
+ /**
+ * @author Michael Dowling and contributors to guzzlehttp/psr7
+ * @author Tobias Nyholm
+ * @author Martijn van der Ven
+ */
+ final class Response implements ResponseInterface
+ {
+ use MessageTrait;
+
+ /** @var array Map of standard HTTP status code/reason phrases */
+ /*private*/ const PHRASES = [
+ 100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing',
+ 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-status', 208 => 'Already Reported',
+ 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 306 => 'Switch Proxy', 307 => 'Temporary Redirect',
+ 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed', 418 => 'I\'m a teapot', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 425 => 'Unordered Collection', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 451 => 'Unavailable For Legal Reasons',
+ 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', 511 => 'Network Authentication Required',
+ ];
+
+ /** @var string */
+ private $reasonPhrase = '';
+
+ /** @var int */
+ private $statusCode;
+
+ /**
+ * @param int $status Status code
+ * @param array $headers Response headers
+ * @param string|resource|StreamInterface|null $body Response body
+ * @param string $version Protocol version
+ * @param string|null $reason Reason phrase (when empty a default will be used based on the status code)
+ */
+ public function __construct(int $status = 200, array $headers = [], $body = null, string $version = '1.1', string $reason = null)
+ {
+ // If we got no body, defer initialization of the stream until Response::getBody()
+ if ('' !== $body && null !== $body) {
+ $this->stream = Stream::create($body);
+ }
+
+ $this->statusCode = $status;
+ $this->setHeaders($headers);
+ if (null === $reason && isset(self::PHRASES[$this->statusCode])) {
+ $this->reasonPhrase = self::PHRASES[$status];
+ } else {
+ $this->reasonPhrase = $reason ?? '';
+ }
+
+ $this->protocol = $version;
+ }
+
+ public function getStatusCode(): int
+ {
+ return $this->statusCode;
+ }
+
+ public function getReasonPhrase(): string
+ {
+ return $this->reasonPhrase;
+ }
+
+ public function withStatus($code, $reasonPhrase = ''): self
+ {
+ if (!\is_int($code) && !\is_string($code)) {
+ throw new \InvalidArgumentException('Status code has to be an integer');
+ }
+
+ $code = (int) $code;
+ if ($code < 100 || $code > 599) {
+ throw new \InvalidArgumentException('Status code has to be an integer between 100 and 599');
+ }
+
+ $new = clone $this;
+ $new->statusCode = $code;
+ if ((null === $reasonPhrase || '' === $reasonPhrase) && isset(self::PHRASES[$new->statusCode])) {
+ $reasonPhrase = self::PHRASES[$new->statusCode];
+ }
+ $new->reasonPhrase = $reasonPhrase;
+
+ return $new;
+ }
+ }
+}
+
+// file: vendor/nyholm/psr7/src/ServerRequest.php
+namespace Nyholm\Psr7 {
+
+ use Psr\Http\Message\{ServerRequestInterface, StreamInterface, UploadedFileInterface, UriInterface};
+
+ /**
+ * @author Michael Dowling and contributors to guzzlehttp/psr7
+ * @author Tobias Nyholm
+ * @author Martijn van der Ven
+ */
+ final class ServerRequest implements ServerRequestInterface
+ {
+ use MessageTrait;
+ use RequestTrait;
+
+ /** @var array */
+ private $attributes = [];
+
+ /** @var array */
+ private $cookieParams = [];
+
+ /** @var array|object|null */
+ private $parsedBody;
+
+ /** @var array */
+ private $queryParams = [];
+
+ /** @var array */
+ private $serverParams;
+
+ /** @var UploadedFileInterface[] */
+ private $uploadedFiles = [];
+
+ /**
+ * @param string $method HTTP method
+ * @param string|UriInterface $uri URI
+ * @param array $headers Request headers
+ * @param string|resource|StreamInterface|null $body Request body
+ * @param string $version Protocol version
+ * @param array $serverParams Typically the $_SERVER superglobal
+ */
+ public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1', array $serverParams = [])
+ {
+ $this->serverParams = $serverParams;
+
+ if (!($uri instanceof UriInterface)) {
+ $uri = new Uri($uri);
+ }
+
+ $this->method = $method;
+ $this->uri = $uri;
+ $this->setHeaders($headers);
+ $this->protocol = $version;
+
+ if (!$this->hasHeader('Host')) {
+ $this->updateHostFromUri();
+ }
+
+ // If we got no body, defer initialization of the stream until ServerRequest::getBody()
+ if ('' !== $body && null !== $body) {
+ $this->stream = Stream::create($body);
+ }
+ }
+
+ public function getServerParams(): array
+ {
+ return $this->serverParams;
+ }
+
+ public function getUploadedFiles(): array
+ {
+ return $this->uploadedFiles;
+ }
+
+ public function withUploadedFiles(array $uploadedFiles)
+ {
+ $new = clone $this;
+ $new->uploadedFiles = $uploadedFiles;
+
+ return $new;
+ }
+
+ public function getCookieParams(): array
+ {
+ return $this->cookieParams;
+ }
+
+ public function withCookieParams(array $cookies)
+ {
+ $new = clone $this;
+ $new->cookieParams = $cookies;
+
+ return $new;
+ }
+
+ public function getQueryParams(): array
+ {
+ return $this->queryParams;
+ }
+
+ public function withQueryParams(array $query)
+ {
+ $new = clone $this;
+ $new->queryParams = $query;
+
+ return $new;
+ }
+
+ public function getParsedBody()
+ {
+ return $this->parsedBody;
+ }
+
+ public function withParsedBody($data)
+ {
+ if (!\is_array($data) && !\is_object($data) && null !== $data) {
+ throw new \InvalidArgumentException('First parameter to withParsedBody MUST be object, array or null');
+ }
+
+ $new = clone $this;
+ $new->parsedBody = $data;
+
+ return $new;
+ }
+
+ public function getAttributes(): array
+ {
+ return $this->attributes;
+ }
+
+ public function getAttribute($attribute, $default = null)
+ {
+ if (false === \array_key_exists($attribute, $this->attributes)) {
+ return $default;
+ }
+
+ return $this->attributes[$attribute];
+ }
+
+ public function withAttribute($attribute, $value): self
+ {
+ $new = clone $this;
+ $new->attributes[$attribute] = $value;
+
+ return $new;
+ }
+
+ public function withoutAttribute($attribute): self
+ {
+ if (false === \array_key_exists($attribute, $this->attributes)) {
+ return $this;
+ }
+
+ $new = clone $this;
+ unset($new->attributes[$attribute]);
+
+ return $new;
+ }
+ }
+}
+
+// file: vendor/nyholm/psr7/src/Stream.php
+namespace Nyholm\Psr7 {
+
+ use Psr\Http\Message\StreamInterface;
+
+ /**
+ * @author Michael Dowling and contributors to guzzlehttp/psr7
+ * @author Tobias Nyholm
+ * @author Martijn van der Ven
+ */
+ final class Stream implements StreamInterface
+ {
+ /** @var resource|null A resource reference */
+ private $stream;
+
+ /** @var bool */
+ private $seekable;
+
+ /** @var bool */
+ private $readable;
+
+ /** @var bool */
+ private $writable;
+
+ /** @var array|mixed|void|null */
+ private $uri;
+
+ /** @var int|null */
+ private $size;
+
+ /** @var array Hash of readable and writable stream types */
+ /*private*/ const READ_WRITE_HASH = [
+ 'read' => [
+ 'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true,
+ 'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true,
+ 'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true,
+ 'x+t' => true, 'c+t' => true, 'a+' => true,
+ ],
+ 'write' => [
+ 'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true,
+ 'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true,
+ 'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true,
+ 'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true,
+ ],
+ ];
+
+ private function __construct()
+ {
+ }
+
+ /**
+ * Creates a new PSR-7 stream.
+ *
+ * @param string|resource|StreamInterface $body
+ *
+ * @return StreamInterface
+ *
+ * @throws \InvalidArgumentException
+ */
+ public static function create($body = ''): StreamInterface
+ {
+ if ($body instanceof StreamInterface) {
+ return $body;
+ }
+
+ if (\is_string($body)) {
+ $resource = \fopen('php://temp', 'rw+');
+ \fwrite($resource, $body);
+ $body = $resource;
+ }
+
+ if (\is_resource($body)) {
+ $new = new self();
+ $new->stream = $body;
+ $meta = \stream_get_meta_data($new->stream);
+ $new->seekable = $meta['seekable'] && 0 === \fseek($new->stream, 0, \SEEK_CUR);
+ $new->readable = isset(self::READ_WRITE_HASH['read'][$meta['mode']]);
+ $new->writable = isset(self::READ_WRITE_HASH['write'][$meta['mode']]);
+ $new->uri = $new->getMetadata('uri');
+
+ return $new;
+ }
+
+ throw new \InvalidArgumentException('First argument to Stream::create() must be a string, resource or StreamInterface.');
+ }
+
+ /**
+ * Closes the stream when the destructed.
+ */
+ public function __destruct()
+ {
+ $this->close();
+ }
+
+ public function __toString(): string
+ {
+ try {
+ if ($this->isSeekable()) {
+ $this->seek(0);
+ }
+
+ return $this->getContents();
+ } catch (\Exception $e) {
+ return '';
+ }
+ }
+
+ public function close() /*:void*/
+ {
+ if (isset($this->stream)) {
+ if (\is_resource($this->stream)) {
+ \fclose($this->stream);
+ }
+ $this->detach();
+ }
+ }
+
+ public function detach()
+ {
+ if (!isset($this->stream)) {
+ return null;
+ }
+
+ $result = $this->stream;
+ unset($this->stream);
+ $this->size = $this->uri = null;
+ $this->readable = $this->writable = $this->seekable = false;
+
+ return $result;
+ }
+
+ public function getSize() /*:?int*/
+ {
+ if (null !== $this->size) {
+ return $this->size;
+ }
+
+ if (!isset($this->stream)) {
+ return null;
+ }
+
+ // Clear the stat cache if the stream has a URI
+ if ($this->uri) {
+ \clearstatcache(true, $this->uri);
+ }
+
+ $stats = \fstat($this->stream);
+ if (isset($stats['size'])) {
+ $this->size = $stats['size'];
+
+ return $this->size;
+ }
+
+ return null;
+ }
+
+ public function tell(): int
+ {
+ if (false === $result = \ftell($this->stream)) {
+ throw new \RuntimeException('Unable to determine stream position');
+ }
+
+ return $result;
+ }
+
+ public function eof(): bool
+ {
+ return !$this->stream || \feof($this->stream);
+ }
+
+ public function isSeekable(): bool
+ {
+ return $this->seekable;
+ }
+
+ public function seek($offset, $whence = \SEEK_SET) /*:void*/
+ {
+ if (!$this->seekable) {
+ throw new \RuntimeException('Stream is not seekable');
+ }
+
+ if (-1 === \fseek($this->stream, $offset, $whence)) {
+ throw new \RuntimeException('Unable to seek to stream position ' . $offset . ' with whence ' . \var_export($whence, true));
+ }
+ }
+
+ public function rewind() /*:void*/
+ {
+ $this->seek(0);
+ }
+
+ public function isWritable(): bool
+ {
+ return $this->writable;
+ }
+
+ public function write($string): int
+ {
+ if (!$this->writable) {
+ throw new \RuntimeException('Cannot write to a non-writable stream');
+ }
+
+ // We can't know the size after writing anything
+ $this->size = null;
+
+ if (false === $result = \fwrite($this->stream, $string)) {
+ throw new \RuntimeException('Unable to write to stream');
+ }
+
+ return $result;
+ }
+
+ public function isReadable(): bool
+ {
+ return $this->readable;
+ }
+
+ public function read($length): string
+ {
+ if (!$this->readable) {
+ throw new \RuntimeException('Cannot read from non-readable stream');
+ }
+
+ return \fread($this->stream, $length);
+ }
+
+ public function getContents(): string
+ {
+ if (!isset($this->stream)) {
+ throw new \RuntimeException('Unable to read stream contents');
+ }
+
+ if (false === $contents = \stream_get_contents($this->stream)) {
+ throw new \RuntimeException('Unable to read stream contents');
+ }
+
+ return $contents;
+ }
+
+ public function getMetadata($key = null)
+ {
+ if (!isset($this->stream)) {
+ return $key ? null : [];
+ }
+
+ $meta = \stream_get_meta_data($this->stream);
+
+ if (null === $key) {
+ return $meta;
+ }
+
+ return $meta[$key] ?? null;
+ }
+ }
+}
+
+// file: vendor/nyholm/psr7/src/UploadedFile.php
+namespace Nyholm\Psr7 {
+
+ use Psr\Http\Message\{StreamInterface, UploadedFileInterface};
+
+ /**
+ * @author Michael Dowling and contributors to guzzlehttp/psr7
+ * @author Tobias Nyholm
+ * @author Martijn van der Ven
+ */
+ final class UploadedFile implements UploadedFileInterface
+ {
+ /** @var array */
+ /*private*/ const ERRORS = [
+ \UPLOAD_ERR_OK => 1,
+ \UPLOAD_ERR_INI_SIZE => 1,
+ \UPLOAD_ERR_FORM_SIZE => 1,
+ \UPLOAD_ERR_PARTIAL => 1,
+ \UPLOAD_ERR_NO_FILE => 1,
+ \UPLOAD_ERR_NO_TMP_DIR => 1,
+ \UPLOAD_ERR_CANT_WRITE => 1,
+ \UPLOAD_ERR_EXTENSION => 1,
+ ];
+
+ /** @var string */
+ private $clientFilename;
+
+ /** @var string */
+ private $clientMediaType;
+
+ /** @var int */
+ private $error;
+
+ /** @var string|null */
+ private $file;
+
+ /** @var bool */
+ private $moved = false;
+
+ /** @var int */
+ private $size;
+
+ /** @var StreamInterface|null */
+ private $stream;
+
+ /**
+ * @param StreamInterface|string|resource $streamOrFile
+ * @param int $size
+ * @param int $errorStatus
+ * @param string|null $clientFilename
+ * @param string|null $clientMediaType
+ */
+ public function __construct($streamOrFile, $size, $errorStatus, $clientFilename = null, $clientMediaType = null)
+ {
+ if (false === \is_int($errorStatus) || !isset(self::ERRORS[$errorStatus])) {
+ throw new \InvalidArgumentException('Upload file error status must be an integer value and one of the "UPLOAD_ERR_*" constants.');
+ }
+
+ if (false === \is_int($size)) {
+ throw new \InvalidArgumentException('Upload file size must be an integer');
+ }
+
+ if (null !== $clientFilename && !\is_string($clientFilename)) {
+ throw new \InvalidArgumentException('Upload file client filename must be a string or null');
+ }
+
+ if (null !== $clientMediaType && !\is_string($clientMediaType)) {
+ throw new \InvalidArgumentException('Upload file client media type must be a string or null');
+ }
+
+ $this->error = $errorStatus;
+ $this->size = $size;
+ $this->clientFilename = $clientFilename;
+ $this->clientMediaType = $clientMediaType;
+
+ if (\UPLOAD_ERR_OK === $this->error) {
+ // Depending on the value set file or stream variable.
+ if (\is_string($streamOrFile)) {
+ $this->file = $streamOrFile;
+ } elseif (\is_resource($streamOrFile)) {
+ $this->stream = Stream::create($streamOrFile);
+ } elseif ($streamOrFile instanceof StreamInterface) {
+ $this->stream = $streamOrFile;
+ } else {
+ throw new \InvalidArgumentException('Invalid stream or file provided for UploadedFile');
+ }
+ }
+ }
+
+ /**
+ * @throws \RuntimeException if is moved or not ok
+ */
+ private function validateActive() /*:void*/
+ {
+ if (\UPLOAD_ERR_OK !== $this->error) {
+ throw new \RuntimeException('Cannot retrieve stream due to upload error');
+ }
+
+ if ($this->moved) {
+ throw new \RuntimeException('Cannot retrieve stream after it has already been moved');
+ }
+ }
+
+ public function getStream(): StreamInterface
+ {
+ $this->validateActive();
+
+ if ($this->stream instanceof StreamInterface) {
+ return $this->stream;
+ }
+
+ $resource = \fopen($this->file, 'r');
+
+ return Stream::create($resource);
+ }
+
+ public function moveTo($targetPath) /*:void*/
+ {
+ $this->validateActive();
+
+ if (!\is_string($targetPath) || '' === $targetPath) {
+ throw new \InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string');
+ }
+
+ if (null !== $this->file) {
+ $this->moved = 'cli' === \PHP_SAPI ? \rename($this->file, $targetPath) : \move_uploaded_file($this->file, $targetPath);
+ } else {
+ $stream = $this->getStream();
+ if ($stream->isSeekable()) {
+ $stream->rewind();
+ }
+
+ // Copy the contents of a stream into another stream until end-of-file.
+ $dest = Stream::create(\fopen($targetPath, 'w'));
+ while (!$stream->eof()) {
+ if (!$dest->write($stream->read(1048576))) {
+ break;
+ }
+ }
+
+ $this->moved = true;
+ }
+
+ if (false === $this->moved) {
+ throw new \RuntimeException(\sprintf('Uploaded file could not be moved to %s', $targetPath));
+ }
+ }
+
+ public function getSize(): int
+ {
+ return $this->size;
+ }
+
+ public function getError(): int
+ {
+ return $this->error;
+ }
+
+ public function getClientFilename() /*:?string*/
+ {
+ return $this->clientFilename;
+ }
+
+ public function getClientMediaType() /*:?string*/
+ {
+ return $this->clientMediaType;
+ }
+ }
+}
+
+// file: vendor/nyholm/psr7/src/Uri.php
+namespace Nyholm\Psr7 {
+
+ use Psr\Http\Message\UriInterface;
+
+ /**
+ * PSR-7 URI implementation.
+ *
+ * @author Michael Dowling
+ * @author Tobias Schultze
+ * @author Matthew Weier O'Phinney
+ * @author Tobias Nyholm
+ * @author Martijn van der Ven
+ */
+ final class Uri implements UriInterface
+ {
+ /*private*/ const SCHEMES = ['http' => 80, 'https' => 443];
+
+ /*private*/ const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~';
+
+ /*private*/ const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
+
+ /** @var string Uri scheme. */
+ private $scheme = '';
+
+ /** @var string Uri user info. */
+ private $userInfo = '';
+
+ /** @var string Uri host. */
+ private $host = '';
+
+ /** @var int|null Uri port. */
+ private $port;
+
+ /** @var string Uri path. */
+ private $path = '';
+
+ /** @var string Uri query string. */
+ private $query = '';
+
+ /** @var string Uri fragment. */
+ private $fragment = '';
+
+ public function __construct(string $uri = '')
+ {
+ if ('' !== $uri) {
+ if (false === $parts = \parse_url($uri)) {
+ throw new \InvalidArgumentException("Unable to parse URI: $uri");
+ }
+
+ // Apply parse_url parts to a URI.
+ $this->scheme = isset($parts['scheme']) ? \strtolower($parts['scheme']) : '';
+ $this->userInfo = $parts['user'] ?? '';
+ $this->host = isset($parts['host']) ? \strtolower($parts['host']) : '';
+ $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null;
+ $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : '';
+ $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : '';
+ $this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : '';
+ if (isset($parts['pass'])) {
+ $this->userInfo .= ':' . $parts['pass'];
+ }
+ }
+ }
+
+ public function __toString(): string
+ {
+ return self::createUriString($this->scheme, $this->getAuthority(), $this->path, $this->query, $this->fragment);
+ }
+
+ public function getScheme(): string
+ {
+ return $this->scheme;
+ }
+
+ public function getAuthority(): string
+ {
+ if ('' === $this->host) {
+ return '';
+ }
+
+ $authority = $this->host;
+ if ('' !== $this->userInfo) {
+ $authority = $this->userInfo . '@' . $authority;
+ }
+
+ if (null !== $this->port) {
+ $authority .= ':' . $this->port;
+ }
+
+ return $authority;
+ }
+
+ public function getUserInfo(): string
+ {
+ return $this->userInfo;
+ }
+
+ public function getHost(): string
+ {
+ return $this->host;
+ }
+
+ public function getPort() /*:?int*/
+ {
+ return $this->port;
+ }
+
+ public function getPath(): string
+ {
+ return $this->path;
+ }
+
+ public function getQuery(): string
+ {
+ return $this->query;
+ }
+
+ public function getFragment(): string
+ {
+ return $this->fragment;
+ }
+
+ public function withScheme($scheme): self
+ {
+ if (!\is_string($scheme)) {
+ throw new \InvalidArgumentException('Scheme must be a string');
+ }
+
+ if ($this->scheme === $scheme = \strtolower($scheme)) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->scheme = $scheme;
+ $new->port = $new->filterPort($new->port);
+
+ return $new;
+ }
+
+ public function withUserInfo($user, $password = null): self
+ {
+ $info = $user;
+ if (null !== $password && '' !== $password) {
+ $info .= ':' . $password;
+ }
+
+ if ($this->userInfo === $info) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->userInfo = $info;
+
+ return $new;
+ }
+
+ public function withHost($host): self
+ {
+ if (!\is_string($host)) {
+ throw new \InvalidArgumentException('Host must be a string');
+ }
+
+ if ($this->host === $host = \strtolower($host)) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->host = $host;
+
+ return $new;
+ }
+
+ public function withPort($port): self
+ {
+ if ($this->port === $port = $this->filterPort($port)) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->port = $port;
+
+ return $new;
+ }
+
+ public function withPath($path): self
+ {
+ if ($this->path === $path = $this->filterPath($path)) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->path = $path;
+
+ return $new;
+ }
+
+ public function withQuery($query): self
+ {
+ if ($this->query === $query = $this->filterQueryAndFragment($query)) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->query = $query;
+
+ return $new;
+ }
+
+ public function withFragment($fragment): self
+ {
+ if ($this->fragment === $fragment = $this->filterQueryAndFragment($fragment)) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->fragment = $fragment;
+
+ return $new;
+ }
+
+ /**
+ * Create a URI string from its various parts.
+ */
+ private static function createUriString(string $scheme, string $authority, string $path, string $query, string $fragment): string
+ {
+ $uri = '';
+ if ('' !== $scheme) {
+ $uri .= $scheme . ':';
+ }
+
+ if ('' !== $authority) {
+ $uri .= '//' . $authority;
+ }
+
+ if ('' !== $path) {
+ if ('/' !== $path[0]) {
+ if ('' !== $authority) {
+ // If the path is rootless and an authority is present, the path MUST be prefixed by "/"
+ $path = '/' . $path;
+ }
+ } elseif (isset($path[1]) && '/' === $path[1]) {
+ if ('' === $authority) {
+ // If the path is starting with more than one "/" and no authority is present, the
+ // starting slashes MUST be reduced to one.
+ $path = '/' . \ltrim($path, '/');
+ }
+ }
+
+ $uri .= $path;
+ }
+
+ if ('' !== $query) {
+ $uri .= '?' . $query;
+ }
+
+ if ('' !== $fragment) {
+ $uri .= '#' . $fragment;
+ }
+
+ return $uri;
+ }
+
+ /**
+ * Is a given port non-standard for the current scheme?
+ */
+ private static function isNonStandardPort(string $scheme, int $port): bool
+ {
+ return !isset(self::SCHEMES[$scheme]) || $port !== self::SCHEMES[$scheme];
+ }
+
+ private function filterPort($port) /*:?int*/
+ {
+ if (null === $port) {
+ return null;
+ }
+
+ $port = (int) $port;
+ if (0 > $port || 0xffff < $port) {
+ throw new \InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 0 and 65535', $port));
+ }
+
+ return self::isNonStandardPort($this->scheme, $port) ? $port : null;
+ }
+
+ private function filterPath($path): string
+ {
+ if (!\is_string($path)) {
+ throw new \InvalidArgumentException('Path must be a string');
+ }
+
+ return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $path);
+ }
+
+ private function filterQueryAndFragment($str): string
+ {
+ if (!\is_string($str)) {
+ throw new \InvalidArgumentException('Query and fragment must be a string');
+ }
+
+ return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $str);
+ }
+
+ private static function rawurlencodeMatchZero(array $match): string
+ {
+ return \rawurlencode($match[0]);
+ }
+ }
+}
+
+// file: vendor/nyholm/psr7-server/src/ServerRequestCreator.php
+namespace Nyholm\Psr7Server {
+
+ use Psr\Http\Message\ServerRequestFactoryInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Message\StreamFactoryInterface;
+ use Psr\Http\Message\StreamInterface;
+ use Psr\Http\Message\UploadedFileFactoryInterface;
+ use Psr\Http\Message\UploadedFileInterface;
+ use Psr\Http\Message\UriFactoryInterface;
+ use Psr\Http\Message\UriInterface;
+
+ /**
+ * @author Tobias Nyholm
+ * @author Martijn van der Ven
+ */
+ final class ServerRequestCreator implements ServerRequestCreatorInterface
+ {
+ private $serverRequestFactory;
+
+ private $uriFactory;
+
+ private $uploadedFileFactory;
+
+ private $streamFactory;
+
+ public function __construct(
+ ServerRequestFactoryInterface $serverRequestFactory,
+ UriFactoryInterface $uriFactory,
+ UploadedFileFactoryInterface $uploadedFileFactory,
+ StreamFactoryInterface $streamFactory
+ ) {
+ $this->serverRequestFactory = $serverRequestFactory;
+ $this->uriFactory = $uriFactory;
+ $this->uploadedFileFactory = $uploadedFileFactory;
+ $this->streamFactory = $streamFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function fromGlobals(): ServerRequestInterface
+ {
+ $server = $_SERVER;
+ if (false === isset($server['REQUEST_METHOD'])) {
+ $server['REQUEST_METHOD'] = 'GET';
+ }
+
+ $headers = \function_exists('getallheaders') ? getallheaders() : static::getHeadersFromServer($_SERVER);
+
+ return $this->fromArrays($server, $headers, $_COOKIE, $_GET, $_POST, $_FILES, \fopen('php://input', 'r') ?: null);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function fromArrays(array $server, array $headers = [], array $cookie = [], array $get = [], array $post = [], array $files = [], $body = null): ServerRequestInterface
+ {
+ $method = $this->getMethodFromEnv($server);
+ $uri = $this->getUriFromEnvWithHTTP($server);
+ $protocol = isset($server['SERVER_PROTOCOL']) ? \str_replace('HTTP/', '', $server['SERVER_PROTOCOL']) : '1.1';
+
+ $serverRequest = $this->serverRequestFactory->createServerRequest($method, $uri, $server);
+ foreach ($headers as $name => $value) {
+ $serverRequest = $serverRequest->withAddedHeader($name, $value);
+ }
+
+ $serverRequest = $serverRequest
+ ->withProtocolVersion($protocol)
+ ->withCookieParams($cookie)
+ ->withQueryParams($get)
+ ->withParsedBody($post)
+ ->withUploadedFiles($this->normalizeFiles($files));
+
+ if (null === $body) {
+ return $serverRequest;
+ }
+
+ if (\is_resource($body)) {
+ $body = $this->streamFactory->createStreamFromResource($body);
+ } elseif (\is_string($body)) {
+ $body = $this->streamFactory->createStream($body);
+ } elseif (!$body instanceof StreamInterface) {
+ throw new \InvalidArgumentException('The $body parameter to ServerRequestCreator::fromArrays must be string, resource or StreamInterface');
+ }
+
+ return $serverRequest->withBody($body);
+ }
+
+ /**
+ * Implementation from Zend\Diactoros\marshalHeadersFromSapi().
+ */
+ public static function getHeadersFromServer(array $server): array
+ {
+ $headers = [];
+ foreach ($server as $key => $value) {
+ // Apache prefixes environment variables with REDIRECT_
+ // if they are added by rewrite rules
+ if (0 === \strpos($key, 'REDIRECT_')) {
+ $key = \substr($key, 9);
+
+ // We will not overwrite existing variables with the
+ // prefixed versions, though
+ if (\array_key_exists($key, $server)) {
+ continue;
+ }
+ }
+
+ if ($value && 0 === \strpos($key, 'HTTP_')) {
+ $name = \strtr(\strtolower(\substr($key, 5)), '_', '-');
+ $headers[$name] = $value;
+
+ continue;
+ }
+
+ if ($value && 0 === \strpos($key, 'CONTENT_')) {
+ $name = 'content-'.\strtolower(\substr($key, 8));
+ $headers[$name] = $value;
+
+ continue;
+ }
+ }
+
+ return $headers;
+ }
+
+ private function getMethodFromEnv(array $environment): string
+ {
+ if (false === isset($environment['REQUEST_METHOD'])) {
+ throw new \InvalidArgumentException('Cannot determine HTTP method');
+ }
+
+ return $environment['REQUEST_METHOD'];
+ }
+
+ private function getUriFromEnvWithHTTP(array $environment): UriInterface
+ {
+ $uri = $this->createUriFromArray($environment);
+ if (empty($uri->getScheme())) {
+ $uri = $uri->withScheme('http');
+ }
+
+ return $uri;
+ }
+
+ /**
+ * Return an UploadedFile instance array.
+ *
+ * @param array $files A array which respect $_FILES structure
+ *
+ * @return UploadedFileInterface[]
+ *
+ * @throws \InvalidArgumentException for unrecognized values
+ */
+ private function normalizeFiles(array $files): array
+ {
+ $normalized = [];
+
+ foreach ($files as $key => $value) {
+ if ($value instanceof UploadedFileInterface) {
+ $normalized[$key] = $value;
+ } elseif (\is_array($value) && isset($value['tmp_name'])) {
+ $normalized[$key] = $this->createUploadedFileFromSpec($value);
+ } elseif (\is_array($value)) {
+ $normalized[$key] = $this->normalizeFiles($value);
+ } else {
+ throw new \InvalidArgumentException('Invalid value in files specification');
+ }
+ }
+
+ return $normalized;
+ }
+
+ /**
+ * Create and return an UploadedFile instance from a $_FILES specification.
+ *
+ * If the specification represents an array of values, this method will
+ * delegate to normalizeNestedFileSpec() and return that return value.
+ *
+ * @param array $value $_FILES struct
+ *
+ * @return array|UploadedFileInterface
+ */
+ private function createUploadedFileFromSpec(array $value)
+ {
+ if (\is_array($value['tmp_name'])) {
+ return $this->normalizeNestedFileSpec($value);
+ }
+
+ try {
+ $stream = $this->streamFactory->createStreamFromFile($value['tmp_name']);
+ } catch (\RuntimeException $e) {
+ $stream = $this->streamFactory->createStream();
+ }
+
+ return $this->uploadedFileFactory->createUploadedFile(
+ $stream,
+ (int) $value['size'],
+ (int) $value['error'],
+ $value['name'],
+ $value['type']
+ );
+ }
+
+ /**
+ * Normalize an array of file specifications.
+ *
+ * Loops through all nested files and returns a normalized array of
+ * UploadedFileInterface instances.
+ *
+ * @param array $files
+ *
+ * @return UploadedFileInterface[]
+ */
+ private function normalizeNestedFileSpec(array $files = []): array
+ {
+ $normalizedFiles = [];
+
+ foreach (\array_keys($files['tmp_name']) as $key) {
+ $spec = [
+ 'tmp_name' => $files['tmp_name'][$key],
+ 'size' => $files['size'][$key],
+ 'error' => $files['error'][$key],
+ 'name' => $files['name'][$key],
+ 'type' => $files['type'][$key],
+ ];
+ $normalizedFiles[$key] = $this->createUploadedFileFromSpec($spec);
+ }
+
+ return $normalizedFiles;
+ }
+
+ /**
+ * Create a new uri from server variable.
+ *
+ * @param array $server typically $_SERVER or similar structure
+ */
+ private function createUriFromArray(array $server): UriInterface
+ {
+ $uri = $this->uriFactory->createUri('');
+
+ if (isset($server['HTTP_X_FORWARDED_PROTO'])) {
+ $uri = $uri->withScheme($server['HTTP_X_FORWARDED_PROTO']);
+ } else {
+ if (isset($server['REQUEST_SCHEME'])) {
+ $uri = $uri->withScheme($server['REQUEST_SCHEME']);
+ } elseif (isset($server['HTTPS'])) {
+ $uri = $uri->withScheme('on' === $server['HTTPS'] ? 'https' : 'http');
+ }
+
+ if (isset($server['SERVER_PORT'])) {
+ $uri = $uri->withPort($server['SERVER_PORT']);
+ }
+ }
+
+ if (isset($server['HTTP_HOST'])) {
+ if (1 === \preg_match('/^(.+)\:(\d+)$/', $server['HTTP_HOST'], $matches)) {
+ $uri = $uri->withHost($matches[1])->withPort($matches[2]);
+ } else {
+ $uri = $uri->withHost($server['HTTP_HOST']);
+ }
+ } elseif (isset($server['SERVER_NAME'])) {
+ $uri = $uri->withHost($server['SERVER_NAME']);
+ }
+
+ if (isset($server['REQUEST_URI'])) {
+ $uri = $uri->withPath(\current(\explode('?', $server['REQUEST_URI'])));
+ }
+
+ if (isset($server['QUERY_STRING'])) {
+ $uri = $uri->withQuery($server['QUERY_STRING']);
+ }
+
+ return $uri;
+ }
+ }
+}
+
+// file: vendor/nyholm/psr7-server/src/ServerRequestCreatorInterface.php
+namespace Nyholm\Psr7Server {
+
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Message\StreamInterface;
+
+ /**
+ * @author Tobias Nyholm
+ * @author Martijn van der Ven
+ */
+ interface ServerRequestCreatorInterface
+ {
+ /**
+ * Create a new server request from the current environment variables.
+ * Defaults to a GET request to minimise the risk of an \InvalidArgumentException.
+ * Includes the current request headers as supplied by the server through `getallheaders()`.
+ * If `getallheaders()` is unavailable on the current server it will fallback to its own `getHeadersFromServer()` method.
+ * Defaults to php://input for the request body.
+ *
+ * @throws \InvalidArgumentException if no valid method or URI can be determined
+ */
+ public function fromGlobals(): ServerRequestInterface;
+
+ /**
+ * Create a new server request from a set of arrays.
+ *
+ * @param array $server typically $_SERVER or similar structure
+ * @param array $headers typically the output of getallheaders() or similar structure
+ * @param array $cookie typically $_COOKIE or similar structure
+ * @param array $get typically $_GET or similar structure
+ * @param array $post typically $_POST or similar structure
+ * @param array $files typically $_FILES or similar structure
+ * @param StreamInterface|resource|string|null $body Typically stdIn
+ *
+ * @throws \InvalidArgumentException if no valid method or URI can be determined
+ */
+ public function fromArrays(
+ array $server,
+ array $headers = [],
+ array $cookie = [],
+ array $get = [],
+ array $post = [],
+ array $files = [],
+ $body = null
+ ): ServerRequestInterface;
+
+ /**
+ * Get parsed headers from ($_SERVER) array.
+ *
+ * @param array $server typically $_SERVER or similar structure
+ *
+ * @return array
+ */
+ public static function getHeadersFromServer(array $server): array;
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Cache/Cache.php
+namespace Tqdev\PhpCrudApi\Cache {
+
+ interface Cache
+ {
+ public function set(string $key, string $value, int $ttl = 0): bool;
+ public function get(string $key): string;
+ public function clear(): bool;
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Cache/CacheFactory.php
+namespace Tqdev\PhpCrudApi\Cache {
+
+ class CacheFactory
+ {
+ public static function create(string $type, string $prefix, string $config): Cache
+ {
+ switch ($type) {
+ case 'TempFile':
+ $cache = new TempFileCache($prefix, $config);
+ break;
+ case 'Redis':
+ $cache = new RedisCache($prefix, $config);
+ break;
+ case 'Memcache':
+ $cache = new MemcacheCache($prefix, $config);
+ break;
+ case 'Memcached':
+ $cache = new MemcachedCache($prefix, $config);
+ break;
+ default:
+ $cache = new NoCache();
+ }
+ return $cache;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Cache/MemcacheCache.php
+namespace Tqdev\PhpCrudApi\Cache {
+
+ class MemcacheCache implements Cache
+ {
+ protected $prefix;
+ protected $memcache;
+
+ public function __construct(string $prefix, string $config)
+ {
+ $this->prefix = $prefix;
+ if ($config == '') {
+ $address = 'localhost';
+ $port = 11211;
+ } elseif (strpos($config, ':') === false) {
+ $address = $config;
+ $port = 11211;
+ } else {
+ list($address, $port) = explode(':', $config);
+ }
+ $this->memcache = $this->create();
+ $this->memcache->addServer($address, $port);
+ }
+
+ protected function create() /*: \Memcache*/
+ {
+ return new \Memcache();
+ }
+
+ public function set(string $key, string $value, int $ttl = 0): bool
+ {
+ return $this->memcache->set($this->prefix . $key, $value, 0, $ttl);
+ }
+
+ public function get(string $key): string
+ {
+ return $this->memcache->get($this->prefix . $key) ?: '';
+ }
+
+ public function clear(): bool
+ {
+ return $this->memcache->flush();
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Cache/MemcachedCache.php
+namespace Tqdev\PhpCrudApi\Cache {
+
+ class MemcachedCache extends MemcacheCache
+ {
+ protected function create() /*: \Memcached*/
+ {
+ return new \Memcached();
+ }
+
+ public function set(string $key, string $value, int $ttl = 0): bool
+ {
+ return $this->memcache->set($this->prefix . $key, $value, $ttl);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Cache/NoCache.php
+namespace Tqdev\PhpCrudApi\Cache {
+
+ class NoCache implements Cache
+ {
+ public function __construct()
+ {
+ }
+
+ public function set(string $key, string $value, int $ttl = 0): bool
+ {
+ return true;
+ }
+
+ public function get(string $key): string
+ {
+ return '';
+ }
+
+ public function clear(): bool
+ {
+ return true;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Cache/RedisCache.php
+namespace Tqdev\PhpCrudApi\Cache {
+
+ class RedisCache implements Cache
+ {
+ protected $prefix;
+ protected $redis;
+
+ public function __construct(string $prefix, string $config)
+ {
+ $this->prefix = $prefix;
+ if ($config == '') {
+ $config = '127.0.0.1';
+ }
+ $params = explode(':', $config, 6);
+ if (isset($params[3])) {
+ $params[3] = null;
+ }
+ $this->redis = new \Redis();
+ call_user_func_array(array($this->redis, 'pconnect'), $params);
+ }
+
+ public function set(string $key, string $value, int $ttl = 0): bool
+ {
+ return $this->redis->set($this->prefix . $key, $value, $ttl);
+ }
+
+ public function get(string $key): string
+ {
+ return $this->redis->get($this->prefix . $key) ?: '';
+ }
+
+ public function clear(): bool
+ {
+ return $this->redis->flushDb();
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Cache/TempFileCache.php
+namespace Tqdev\PhpCrudApi\Cache {
+
+ class TempFileCache implements Cache
+ {
+ const SUFFIX = 'cache';
+
+ private $path;
+ private $segments;
+
+ public function __construct(string $prefix, string $config)
+ {
+ $this->segments = [];
+ $s = DIRECTORY_SEPARATOR;
+ $ps = PATH_SEPARATOR;
+ if ($config == '') {
+ $this->path = sys_get_temp_dir() . $s . $prefix . self::SUFFIX;
+ } elseif (strpos($config, $ps) === false) {
+ $this->path = $config;
+ } else {
+ list($path, $segments) = explode($ps, $config);
+ $this->path = $path;
+ $this->segments = explode(',', $segments);
+ }
+ if (file_exists($this->path) && is_dir($this->path)) {
+ $this->clean($this->path, array_filter($this->segments), strlen(md5('')), false);
+ }
+ }
+
+ private function getFileName(string $key): string
+ {
+ $s = DIRECTORY_SEPARATOR;
+ $md5 = md5($key);
+ $filename = rtrim($this->path, $s) . $s;
+ $i = 0;
+ foreach ($this->segments as $segment) {
+ $filename .= substr($md5, $i, $segment) . $s;
+ $i += $segment;
+ }
+ $filename .= substr($md5, $i);
+ return $filename;
+ }
+
+ public function set(string $key, string $value, int $ttl = 0): bool
+ {
+ $filename = $this->getFileName($key);
+ $dirname = dirname($filename);
+ if (!file_exists($dirname)) {
+ if (!mkdir($dirname, 0755, true)) {
+ return false;
+ }
+ }
+ $string = $ttl . '|' . $value;
+ return $this->filePutContents($filename, $string) !== false;
+ }
+
+ private function filePutContents($filename, $string)
+ {
+ return file_put_contents($filename, $string, LOCK_EX);
+ }
+
+ private function fileGetContents($filename)
+ {
+ $file = fopen($filename, 'rb');
+ if ($file === false) {
+ return false;
+ }
+ $lock = flock($file, LOCK_SH);
+ if (!$lock) {
+ fclose($file);
+ return false;
+ }
+ $string = '';
+ while (!feof($file)) {
+ $string .= fread($file, 8192);
+ }
+ flock($file, LOCK_UN);
+ fclose($file);
+ return $string;
+ }
+
+ private function getString($filename): string
+ {
+ $data = $this->fileGetContents($filename);
+ if ($data === false) {
+ return '';
+ }
+ if (strpos($data, '|') === false) {
+ return '';
+ }
+ list($ttl, $string) = explode('|', $data, 2);
+ if ($ttl > 0 && time() - filemtime($filename) > $ttl) {
+ return '';
+ }
+ return $string;
+ }
+
+ public function get(string $key): string
+ {
+ $filename = $this->getFileName($key);
+ if (!file_exists($filename)) {
+ return '';
+ }
+ $string = $this->getString($filename);
+ if ($string == null) {
+ return '';
+ }
+ return $string;
+ }
+
+ private function clean(string $path, array $segments, int $len, bool $all) /*: void*/
+ {
+ $entries = scandir($path);
+ foreach ($entries as $entry) {
+ if ($entry === '.' || $entry === '..') {
+ continue;
+ }
+ $filename = $path . DIRECTORY_SEPARATOR . $entry;
+ if (count($segments) == 0) {
+ if (strlen($entry) != $len) {
+ continue;
+ }
+ if (file_exists($filename) && is_file($filename)) {
+ if ($all || $this->getString($filename) == null) {
+ @unlink($filename);
+ }
+ }
+ } else {
+ if (strlen($entry) != $segments[0]) {
+ continue;
+ }
+ if (file_exists($filename) && is_dir($filename)) {
+ $this->clean($filename, array_slice($segments, 1), $len - $segments[0], $all);
+ @rmdir($filename);
+ }
+ }
+ }
+ }
+
+ public function clear(): bool
+ {
+ if (!file_exists($this->path) || !is_dir($this->path)) {
+ return false;
+ }
+ $this->clean($this->path, array_filter($this->segments), strlen(md5('')), true);
+ return true;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedColumn.php
+namespace Tqdev\PhpCrudApi\Column\Reflection {
+
+ use Tqdev\PhpCrudApi\Database\GenericReflection;
+
+ class ReflectedColumn implements \JsonSerializable
+ {
+ const DEFAULT_LENGTH = 255;
+ const DEFAULT_PRECISION = 19;
+ const DEFAULT_SCALE = 4;
+
+ private $name;
+ private $type;
+ private $length;
+ private $precision;
+ private $scale;
+ private $nullable;
+ private $pk;
+ private $fk;
+
+ public function __construct(string $name, string $type, int $length, int $precision, int $scale, bool $nullable, bool $pk, string $fk)
+ {
+ $this->name = $name;
+ $this->type = $type;
+ $this->length = $length;
+ $this->precision = $precision;
+ $this->scale = $scale;
+ $this->nullable = $nullable;
+ $this->pk = $pk;
+ $this->fk = $fk;
+ $this->sanitize();
+ }
+
+ private static function parseColumnType(string $columnType, int &$length, int &$precision, int &$scale) /*: void*/
+ {
+ if (!$columnType) {
+ return;
+ }
+ $pos = strpos($columnType, '(');
+ if ($pos) {
+ $dataSize = rtrim(substr($columnType, $pos + 1), ')');
+ if ($length) {
+ $length = (int) $dataSize;
+ } else {
+ $pos = strpos($dataSize, ',');
+ if ($pos) {
+ $precision = (int) substr($dataSize, 0, $pos);
+ $scale = (int) substr($dataSize, $pos + 1);
+ } else {
+ $precision = (int) $dataSize;
+ $scale = 0;
+ }
+ }
+ }
+ }
+
+ private static function getDataSize(int $length, int $precision, int $scale): string
+ {
+ $dataSize = '';
+ if ($length) {
+ $dataSize = $length;
+ } elseif ($precision) {
+ if ($scale) {
+ $dataSize = $precision . ',' . $scale;
+ } else {
+ $dataSize = $precision;
+ }
+ }
+ return $dataSize;
+ }
+
+ public static function fromReflection(GenericReflection $reflection, array $columnResult): ReflectedColumn
+ {
+ $name = $columnResult['COLUMN_NAME'];
+ $dataType = $columnResult['DATA_TYPE'];
+ $length = (int) $columnResult['CHARACTER_MAXIMUM_LENGTH'];
+ $precision = (int) $columnResult['NUMERIC_PRECISION'];
+ $scale = (int) $columnResult['NUMERIC_SCALE'];
+ $columnType = $columnResult['COLUMN_TYPE'];
+ self::parseColumnType($columnType, $length, $precision, $scale);
+ $dataSize = self::getDataSize($length, $precision, $scale);
+ $type = $reflection->toJdbcType($dataType, $dataSize);
+ $nullable = in_array(strtoupper($columnResult['IS_NULLABLE']), ['TRUE', 'YES', 'T', 'Y', '1']);
+ $pk = false;
+ $fk = '';
+ return new ReflectedColumn($name, $type, $length, $precision, $scale, $nullable, $pk, $fk);
+ }
+
+ public static function fromJson(/* object */$json): ReflectedColumn
+ {
+ $name = $json->name;
+ $type = $json->type;
+ $length = isset($json->length) ? (int) $json->length : 0;
+ $precision = isset($json->precision) ? (int) $json->precision : 0;
+ $scale = isset($json->scale) ? (int) $json->scale : 0;
+ $nullable = isset($json->nullable) ? (bool) $json->nullable : false;
+ $pk = isset($json->pk) ? (bool) $json->pk : false;
+ $fk = isset($json->fk) ? $json->fk : '';
+ return new ReflectedColumn($name, $type, $length, $precision, $scale, $nullable, $pk, $fk);
+ }
+
+ private function sanitize()
+ {
+ $this->length = $this->hasLength() ? $this->getLength() : 0;
+ $this->precision = $this->hasPrecision() ? $this->getPrecision() : 0;
+ $this->scale = $this->hasScale() ? $this->getScale() : 0;
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function getNullable(): bool
+ {
+ return $this->nullable;
+ }
+
+ public function getType(): string
+ {
+ return $this->type;
+ }
+
+ public function getLength(): int
+ {
+ return $this->length ?: self::DEFAULT_LENGTH;
+ }
+
+ public function getPrecision(): int
+ {
+ return $this->precision ?: self::DEFAULT_PRECISION;
+ }
+
+ public function getScale(): int
+ {
+ return $this->scale ?: self::DEFAULT_SCALE;
+ }
+
+ public function hasLength(): bool
+ {
+ return in_array($this->type, ['varchar', 'varbinary']);
+ }
+
+ public function hasPrecision(): bool
+ {
+ return $this->type == 'decimal';
+ }
+
+ public function hasScale(): bool
+ {
+ return $this->type == 'decimal';
+ }
+
+ public function isBinary(): bool
+ {
+ return in_array($this->type, ['blob', 'varbinary']);
+ }
+
+ public function isBoolean(): bool
+ {
+ return $this->type == 'boolean';
+ }
+
+ public function isGeometry(): bool
+ {
+ return $this->type == 'geometry';
+ }
+
+ public function isInteger(): bool
+ {
+ return in_array($this->type, ['integer', 'bigint', 'smallint', 'tinyint']);
+ }
+
+ public function setPk($value) /*: void*/
+ {
+ $this->pk = $value;
+ }
+
+ public function getPk(): bool
+ {
+ return $this->pk;
+ }
+
+ public function setFk($value) /*: void*/
+ {
+ $this->fk = $value;
+ }
+
+ public function getFk(): string
+ {
+ return $this->fk;
+ }
+
+ public function serialize()
+ {
+ return [
+ 'name' => $this->name,
+ 'type' => $this->type,
+ 'length' => $this->length,
+ 'precision' => $this->precision,
+ 'scale' => $this->scale,
+ 'nullable' => $this->nullable,
+ 'pk' => $this->pk,
+ 'fk' => $this->fk,
+ ];
+ }
+
+ public function jsonSerialize()
+ {
+ return array_filter($this->serialize());
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedDatabase.php
+namespace Tqdev\PhpCrudApi\Column\Reflection {
+
+ use Tqdev\PhpCrudApi\Database\GenericReflection;
+
+ class ReflectedDatabase implements \JsonSerializable
+ {
+ private $tableTypes;
+
+ public function __construct(array $tableTypes)
+ {
+ $this->tableTypes = $tableTypes;
+ }
+
+ public static function fromReflection(GenericReflection $reflection): ReflectedDatabase
+ {
+ $tableTypes = [];
+ foreach ($reflection->getTables() as $table) {
+ $tableName = $table['TABLE_NAME'];
+ $tableType = $table['TABLE_TYPE'];
+ if (in_array($tableName, $reflection->getIgnoredTables())) {
+ continue;
+ }
+ $tableTypes[$tableName] = $tableType;
+ }
+ return new ReflectedDatabase($tableTypes);
+ }
+
+ public static function fromJson(/* object */$json): ReflectedDatabase
+ {
+ $tableTypes = (array) $json->tables;
+ return new ReflectedDatabase($tableTypes);
+ }
+
+ public function hasTable(string $tableName): bool
+ {
+ return isset($this->tableTypes[$tableName]);
+ }
+
+ public function getType(string $tableName): string
+ {
+ return isset($this->tableTypes[$tableName]) ? $this->tableTypes[$tableName] : '';
+ }
+
+ public function getTableNames(): array
+ {
+ return array_keys($this->tableTypes);
+ }
+
+ public function removeTable(string $tableName): bool
+ {
+ if (!isset($this->tableTypes[$tableName])) {
+ return false;
+ }
+ unset($this->tableTypes[$tableName]);
+ return true;
+ }
+
+ public function serialize()
+ {
+ return [
+ 'tables' => $this->tableTypes,
+ ];
+ }
+
+ public function jsonSerialize()
+ {
+ return $this->serialize();
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedTable.php
+namespace Tqdev\PhpCrudApi\Column\Reflection {
+
+ use Tqdev\PhpCrudApi\Database\GenericReflection;
+
+ class ReflectedTable implements \JsonSerializable
+ {
+ private $name;
+ private $type;
+ private $columns;
+ private $pk;
+ private $fks;
+
+ public function __construct(string $name, string $type, array $columns)
+ {
+ $this->name = $name;
+ $this->type = $type;
+ // set columns
+ $this->columns = [];
+ foreach ($columns as $column) {
+ $columnName = $column->getName();
+ $this->columns[$columnName] = $column;
+ }
+ // set primary key
+ $this->pk = null;
+ foreach ($columns as $column) {
+ if ($column->getPk() == true) {
+ $this->pk = $column;
+ }
+ }
+ // set foreign keys
+ $this->fks = [];
+ foreach ($columns as $column) {
+ $columnName = $column->getName();
+ $referencedTableName = $column->getFk();
+ if ($referencedTableName != '') {
+ $this->fks[$columnName] = $referencedTableName;
+ }
+ }
+ }
+
+ public static function fromReflection(GenericReflection $reflection, string $name, string $type): ReflectedTable
+ {
+ // set columns
+ $columns = [];
+ foreach ($reflection->getTableColumns($name, $type) as $tableColumn) {
+ $column = ReflectedColumn::fromReflection($reflection, $tableColumn);
+ $columns[$column->getName()] = $column;
+ }
+ // set primary key
+ $columnName = false;
+ if ($type == 'view') {
+ $columnName = 'id';
+ } else {
+ $columnNames = $reflection->getTablePrimaryKeys($name);
+ if (count($columnNames) == 1) {
+ $columnName = $columnNames[0];
+ }
+ }
+ if ($columnName && isset($columns[$columnName])) {
+ $pk = $columns[$columnName];
+ $pk->setPk(true);
+ }
+ // set foreign keys
+ if ($type == 'view') {
+ $tables = $reflection->getTables();
+ foreach ($columns as $columnName => $column) {
+ if (substr($columnName, -3) == '_id') {
+ foreach ($tables as $table) {
+ $tableName = $table['TABLE_NAME'];
+ $suffix = $tableName . '_id';
+ if (substr($columnName, -1 * strlen($suffix)) == $suffix) {
+ $column->setFk($tableName);
+ }
+ }
+ }
+ }
+ } else {
+ $fks = $reflection->getTableForeignKeys($name);
+ foreach ($fks as $columnName => $table) {
+ $columns[$columnName]->setFk($table);
+ }
+ }
+ return new ReflectedTable($name, $type, array_values($columns));
+ }
+
+ public static function fromJson( /* object */$json): ReflectedTable
+ {
+ $name = $json->name;
+ $type = isset($json->type) ? $json->type : 'table';
+ $columns = [];
+ if (isset($json->columns) && is_array($json->columns)) {
+ foreach ($json->columns as $column) {
+ $columns[] = ReflectedColumn::fromJson($column);
+ }
+ }
+ return new ReflectedTable($name, $type, $columns);
+ }
+
+ public function hasColumn(string $columnName): bool
+ {
+ return isset($this->columns[$columnName]);
+ }
+
+ public function hasPk(): bool
+ {
+ return $this->pk != null;
+ }
+
+ public function getPk() /*: ?ReflectedColumn */
+ {
+ return $this->pk;
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function getType(): string
+ {
+ return $this->type;
+ }
+
+ public function getColumnNames(): array
+ {
+ return array_keys($this->columns);
+ }
+
+ public function getColumn($columnName): ReflectedColumn
+ {
+ return $this->columns[$columnName];
+ }
+
+ public function getFksTo(string $tableName): array
+ {
+ $columns = array();
+ foreach ($this->fks as $columnName => $referencedTableName) {
+ if ($tableName == $referencedTableName && !is_null($this->columns[$columnName])) {
+ $columns[] = $this->columns[$columnName];
+ }
+ }
+ return $columns;
+ }
+
+ public function removeColumn(string $columnName): bool
+ {
+ if (!isset($this->columns[$columnName])) {
+ return false;
+ }
+ unset($this->columns[$columnName]);
+ return true;
+ }
+
+ public function serialize()
+ {
+ return [
+ 'name' => $this->name,
+ 'type' => $this->type,
+ 'columns' => array_values($this->columns),
+ ];
+ }
+
+ public function jsonSerialize()
+ {
+ return $this->serialize();
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Column/DefinitionService.php
+namespace Tqdev\PhpCrudApi\Column {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+ use Tqdev\PhpCrudApi\Database\GenericDB;
+
+ class DefinitionService
+ {
+ private $db;
+ private $reflection;
+
+ public function __construct(GenericDB $db, ReflectionService $reflection)
+ {
+ $this->db = $db;
+ $this->reflection = $reflection;
+ }
+
+ public function updateTable(string $tableName, /* object */ $changes): bool
+ {
+ $table = $this->reflection->getTable($tableName);
+ $newTable = ReflectedTable::fromJson((object) array_merge((array) $table->jsonSerialize(), (array) $changes));
+ if ($table->getName() != $newTable->getName()) {
+ if (!$this->db->definition()->renameTable($table->getName(), $newTable->getName())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public function updateColumn(string $tableName, string $columnName, /* object */ $changes): bool
+ {
+ $table = $this->reflection->getTable($tableName);
+ $column = $table->getColumn($columnName);
+
+ // remove constraints on other column
+ $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes));
+ if ($newColumn->getPk() != $column->getPk() && $table->hasPk()) {
+ $oldColumn = $table->getPk();
+ if ($oldColumn->getName() != $columnName) {
+ $oldColumn->setPk(false);
+ if (!$this->db->definition()->removeColumnPrimaryKey($table->getName(), $oldColumn->getName(), $oldColumn)) {
+ return false;
+ }
+ }
+ }
+
+ // remove constraints
+ $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), ['pk' => false, 'fk' => false]));
+ if ($newColumn->getPk() != $column->getPk() && !$newColumn->getPk()) {
+ if (!$this->db->definition()->removeColumnPrimaryKey($table->getName(), $column->getName(), $newColumn)) {
+ return false;
+ }
+ }
+ if ($newColumn->getFk() != $column->getFk() && !$newColumn->getFk()) {
+ if (!$this->db->definition()->removeColumnForeignKey($table->getName(), $column->getName(), $newColumn)) {
+ return false;
+ }
+ }
+
+ // name and type
+ $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes));
+ $newColumn->setPk(false);
+ $newColumn->setFk('');
+ if ($newColumn->getName() != $column->getName()) {
+ if (!$this->db->definition()->renameColumn($table->getName(), $column->getName(), $newColumn)) {
+ return false;
+ }
+ }
+ if (
+ $newColumn->getType() != $column->getType() ||
+ $newColumn->getLength() != $column->getLength() ||
+ $newColumn->getPrecision() != $column->getPrecision() ||
+ $newColumn->getScale() != $column->getScale()
+ ) {
+ if (!$this->db->definition()->retypeColumn($table->getName(), $newColumn->getName(), $newColumn)) {
+ return false;
+ }
+ }
+ if ($newColumn->getNullable() != $column->getNullable()) {
+ if (!$this->db->definition()->setColumnNullable($table->getName(), $newColumn->getName(), $newColumn)) {
+ return false;
+ }
+ }
+
+ // add constraints
+ $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes));
+ if ($newColumn->getFk()) {
+ if (!$this->db->definition()->addColumnForeignKey($table->getName(), $newColumn->getName(), $newColumn)) {
+ return false;
+ }
+ }
+ if ($newColumn->getPk()) {
+ if (!$this->db->definition()->addColumnPrimaryKey($table->getName(), $newColumn->getName(), $newColumn)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public function addTable(/* object */$definition)
+ {
+ $newTable = ReflectedTable::fromJson($definition);
+ if (!$this->db->definition()->addTable($newTable)) {
+ return false;
+ }
+ return true;
+ }
+
+ public function addColumn(string $tableName, /* object */ $definition)
+ {
+ $newColumn = ReflectedColumn::fromJson($definition);
+ if (!$this->db->definition()->addColumn($tableName, $newColumn)) {
+ return false;
+ }
+ if ($newColumn->getFk()) {
+ if (!$this->db->definition()->addColumnForeignKey($tableName, $newColumn->getName(), $newColumn)) {
+ return false;
+ }
+ }
+ if ($newColumn->getPk()) {
+ if (!$this->db->definition()->addColumnPrimaryKey($tableName, $newColumn->getName(), $newColumn)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public function removeTable(string $tableName)
+ {
+ if (!$this->db->definition()->removeTable($tableName)) {
+ return false;
+ }
+ return true;
+ }
+
+ public function removeColumn(string $tableName, string $columnName)
+ {
+ $table = $this->reflection->getTable($tableName);
+ $newColumn = $table->getColumn($columnName);
+ if ($newColumn->getPk()) {
+ $newColumn->setPk(false);
+ if (!$this->db->definition()->removeColumnPrimaryKey($table->getName(), $newColumn->getName(), $newColumn)) {
+ return false;
+ }
+ }
+ if ($newColumn->getFk()) {
+ $newColumn->setFk("");
+ if (!$this->db->definition()->removeColumnForeignKey($tableName, $columnName, $newColumn)) {
+ return false;
+ }
+ }
+ if (!$this->db->definition()->removeColumn($tableName, $columnName)) {
+ return false;
+ }
+ return true;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Column/ReflectionService.php
+namespace Tqdev\PhpCrudApi\Column {
+
+ use Tqdev\PhpCrudApi\Cache\Cache;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedDatabase;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+ use Tqdev\PhpCrudApi\Database\GenericDB;
+
+ class ReflectionService
+ {
+ private $db;
+ private $cache;
+ private $ttl;
+ private $database;
+ private $tables;
+
+ public function __construct(GenericDB $db, Cache $cache, int $ttl)
+ {
+ $this->db = $db;
+ $this->cache = $cache;
+ $this->ttl = $ttl;
+ $this->database = null;
+ $this->tables = [];
+ }
+
+ private function database(): ReflectedDatabase
+ {
+ if ($this->database) {
+ return $this->database;
+ }
+ $this->database = $this->loadDatabase(true);
+ return $this->database;
+ }
+
+ private function loadDatabase(bool $useCache): ReflectedDatabase
+ {
+ $key = sprintf('%s-ReflectedDatabase', $this->db->getCacheKey());
+ $data = $useCache ? $this->cache->get($key) : '';
+ if ($data != '') {
+ $database = ReflectedDatabase::fromJson(json_decode(gzuncompress($data)));
+ } else {
+ $database = ReflectedDatabase::fromReflection($this->db->reflection());
+ $data = gzcompress(json_encode($database, JSON_UNESCAPED_UNICODE));
+ $this->cache->set($key, $data, $this->ttl);
+ }
+ return $database;
+ }
+
+ private function loadTable(string $tableName, bool $useCache): ReflectedTable
+ {
+ $key = sprintf('%s-ReflectedTable(%s)', $this->db->getCacheKey(), $tableName);
+ $data = $useCache ? $this->cache->get($key) : '';
+ if ($data != '') {
+ $table = ReflectedTable::fromJson(json_decode(gzuncompress($data)));
+ } else {
+ $tableType = $this->database()->getType($tableName);
+ $table = ReflectedTable::fromReflection($this->db->reflection(), $tableName, $tableType);
+ $data = gzcompress(json_encode($table, JSON_UNESCAPED_UNICODE));
+ $this->cache->set($key, $data, $this->ttl);
+ }
+ return $table;
+ }
+
+ public function refreshTables()
+ {
+ $this->database = $this->loadDatabase(false);
+ }
+
+ public function refreshTable(string $tableName)
+ {
+ $this->tables[$tableName] = $this->loadTable($tableName, false);
+ }
+
+ public function hasTable(string $tableName): bool
+ {
+ return $this->database()->hasTable($tableName);
+ }
+
+ public function getType(string $tableName): string
+ {
+ return $this->database()->getType($tableName);
+ }
+
+ public function getTable(string $tableName): ReflectedTable
+ {
+ if (!isset($this->tables[$tableName])) {
+ $this->tables[$tableName] = $this->loadTable($tableName, true);
+ }
+ return $this->tables[$tableName];
+ }
+
+ public function getTableNames(): array
+ {
+ return $this->database()->getTableNames();
+ }
+
+ public function removeTable(string $tableName): bool
+ {
+ unset($this->tables[$tableName]);
+ return $this->database()->removeTable($tableName);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Controller/CacheController.php
+namespace Tqdev\PhpCrudApi\Controller {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Tqdev\PhpCrudApi\Cache\Cache;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+
+ class CacheController
+ {
+ private $cache;
+ private $responder;
+
+ public function __construct(Router $router, Responder $responder, Cache $cache)
+ {
+ $router->register('GET', '/cache/clear', array($this, 'clear'));
+ $this->cache = $cache;
+ $this->responder = $responder;
+ }
+
+ public function clear(ServerRequestInterface $request): ResponseInterface
+ {
+ return $this->responder->success($this->cache->clear());
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Controller/ColumnController.php
+namespace Tqdev\PhpCrudApi\Controller {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Tqdev\PhpCrudApi\Column\DefinitionService;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class ColumnController
+ {
+ private $responder;
+ private $reflection;
+ private $definition;
+
+ public function __construct(Router $router, Responder $responder, ReflectionService $reflection, DefinitionService $definition)
+ {
+ $router->register('GET', '/columns', array($this, 'getDatabase'));
+ $router->register('GET', '/columns/*', array($this, 'getTable'));
+ $router->register('GET', '/columns/*/*', array($this, 'getColumn'));
+ $router->register('PUT', '/columns/*', array($this, 'updateTable'));
+ $router->register('PUT', '/columns/*/*', array($this, 'updateColumn'));
+ $router->register('POST', '/columns', array($this, 'addTable'));
+ $router->register('POST', '/columns/*', array($this, 'addColumn'));
+ $router->register('DELETE', '/columns/*', array($this, 'removeTable'));
+ $router->register('DELETE', '/columns/*/*', array($this, 'removeColumn'));
+ $this->responder = $responder;
+ $this->reflection = $reflection;
+ $this->definition = $definition;
+ }
+
+ public function getDatabase(ServerRequestInterface $request): ResponseInterface
+ {
+ $tables = [];
+ foreach ($this->reflection->getTableNames() as $table) {
+ $tables[] = $this->reflection->getTable($table);
+ }
+ $database = ['tables' => $tables];
+ return $this->responder->success($database);
+ }
+
+ public function getTable(ServerRequestInterface $request): ResponseInterface
+ {
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ if (!$this->reflection->hasTable($tableName)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
+ }
+ $table = $this->reflection->getTable($tableName);
+ return $this->responder->success($table);
+ }
+
+ public function getColumn(ServerRequestInterface $request): ResponseInterface
+ {
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ $columnName = RequestUtils::getPathSegment($request, 3);
+ if (!$this->reflection->hasTable($tableName)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
+ }
+ $table = $this->reflection->getTable($tableName);
+ if (!$table->hasColumn($columnName)) {
+ return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
+ }
+ $column = $table->getColumn($columnName);
+ return $this->responder->success($column);
+ }
+
+ public function updateTable(ServerRequestInterface $request): ResponseInterface
+ {
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ if (!$this->reflection->hasTable($tableName)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
+ }
+ $success = $this->definition->updateTable($tableName, $request->getParsedBody());
+ if ($success) {
+ $this->reflection->refreshTables();
+ }
+ return $this->responder->success($success);
+ }
+
+ public function updateColumn(ServerRequestInterface $request): ResponseInterface
+ {
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ $columnName = RequestUtils::getPathSegment($request, 3);
+ if (!$this->reflection->hasTable($tableName)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
+ }
+ $table = $this->reflection->getTable($tableName);
+ if (!$table->hasColumn($columnName)) {
+ return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
+ }
+ $success = $this->definition->updateColumn($tableName, $columnName, $request->getParsedBody());
+ if ($success) {
+ $this->reflection->refreshTable($tableName);
+ }
+ return $this->responder->success($success);
+ }
+
+ public function addTable(ServerRequestInterface $request): ResponseInterface
+ {
+ $tableName = $request->getParsedBody()->name;
+ if ($this->reflection->hasTable($tableName)) {
+ return $this->responder->error(ErrorCode::TABLE_ALREADY_EXISTS, $tableName);
+ }
+ $success = $this->definition->addTable($request->getParsedBody());
+ if ($success) {
+ $this->reflection->refreshTables();
+ }
+ return $this->responder->success($success);
+ }
+
+ public function addColumn(ServerRequestInterface $request): ResponseInterface
+ {
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ if (!$this->reflection->hasTable($tableName)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
+ }
+ $columnName = $request->getParsedBody()->name;
+ $table = $this->reflection->getTable($tableName);
+ if ($table->hasColumn($columnName)) {
+ return $this->responder->error(ErrorCode::COLUMN_ALREADY_EXISTS, $columnName);
+ }
+ $success = $this->definition->addColumn($tableName, $request->getParsedBody());
+ if ($success) {
+ $this->reflection->refreshTable($tableName);
+ }
+ return $this->responder->success($success);
+ }
+
+ public function removeTable(ServerRequestInterface $request): ResponseInterface
+ {
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ if (!$this->reflection->hasTable($tableName)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
+ }
+ $success = $this->definition->removeTable($tableName);
+ if ($success) {
+ $this->reflection->refreshTables();
+ }
+ return $this->responder->success($success);
+ }
+
+ public function removeColumn(ServerRequestInterface $request): ResponseInterface
+ {
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ $columnName = RequestUtils::getPathSegment($request, 3);
+ if (!$this->reflection->hasTable($tableName)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
+ }
+ $table = $this->reflection->getTable($tableName);
+ if (!$table->hasColumn($columnName)) {
+ return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
+ }
+ $success = $this->definition->removeColumn($tableName, $columnName);
+ if ($success) {
+ $this->reflection->refreshTable($tableName);
+ }
+ return $this->responder->success($success);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Controller/GeoJsonController.php
+namespace Tqdev\PhpCrudApi\Controller {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Tqdev\PhpCrudApi\GeoJson\GeoJsonService;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class GeoJsonController
+ {
+ private $service;
+ private $responder;
+
+ public function __construct(Router $router, Responder $responder, GeoJsonService $service)
+ {
+ $router->register('GET', '/geojson/*', array($this, '_list'));
+ $router->register('GET', '/geojson/*/*', array($this, 'read'));
+ $this->service = $service;
+ $this->responder = $responder;
+ }
+
+ public function _list(ServerRequestInterface $request): ResponseInterface
+ {
+ $table = RequestUtils::getPathSegment($request, 2);
+ $params = RequestUtils::getParams($request);
+ if (!$this->service->hasTable($table)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
+ }
+ return $this->responder->success($this->service->_list($table, $params));
+ }
+
+ public function read(ServerRequestInterface $request): ResponseInterface
+ {
+ $table = RequestUtils::getPathSegment($request, 2);
+ if (!$this->service->hasTable($table)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
+ }
+ if ($this->service->getType($table) != 'table') {
+ return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
+ }
+ $id = RequestUtils::getPathSegment($request, 3);
+ $params = RequestUtils::getParams($request);
+ if (strpos($id, ',') !== false) {
+ $ids = explode(',', $id);
+ $result = (object) array('type' => 'FeatureCollection', 'features' => array());
+ for ($i = 0; $i < count($ids); $i++) {
+ array_push($result->features, $this->service->read($table, $ids[$i], $params));
+ }
+ return $this->responder->success($result);
+ } else {
+ $response = $this->service->read($table, $id, $params);
+ if ($response === null) {
+ return $this->responder->error(ErrorCode::RECORD_NOT_FOUND, $id);
+ }
+ return $this->responder->success($response);
+ }
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Controller/JsonResponder.php
+namespace Tqdev\PhpCrudApi\Controller {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Tqdev\PhpCrudApi\Record\Document\ErrorDocument;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\ResponseFactory;
+
+ class JsonResponder implements Responder
+ {
+ public function error(int $error, string $argument, $details = null): ResponseInterface
+ {
+ $errorCode = new ErrorCode($error);
+ $status = $errorCode->getStatus();
+ $document = new ErrorDocument($errorCode, $argument, $details);
+ return ResponseFactory::fromObject($status, $document);
+ }
+
+ public function success($result): ResponseInterface
+ {
+ return ResponseFactory::fromObject(ResponseFactory::OK, $result);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Controller/OpenApiController.php
+namespace Tqdev\PhpCrudApi\Controller {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\OpenApi\OpenApiService;
+
+ class OpenApiController
+ {
+ private $openApi;
+ private $responder;
+
+ public function __construct(Router $router, Responder $responder, OpenApiService $openApi)
+ {
+ $router->register('GET', '/openapi', array($this, 'openapi'));
+ $this->openApi = $openApi;
+ $this->responder = $responder;
+ }
+
+ public function openapi(ServerRequestInterface $request): ResponseInterface
+ {
+ return $this->responder->success($this->openApi->get());
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Controller/RecordController.php
+namespace Tqdev\PhpCrudApi\Controller {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\Record\RecordService;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class RecordController
+ {
+ private $service;
+ private $responder;
+
+ public function __construct(Router $router, Responder $responder, RecordService $service)
+ {
+ $router->register('GET', '/records/*', array($this, '_list'));
+ $router->register('POST', '/records/*', array($this, 'create'));
+ $router->register('GET', '/records/*/*', array($this, 'read'));
+ $router->register('PUT', '/records/*/*', array($this, 'update'));
+ $router->register('DELETE', '/records/*/*', array($this, 'delete'));
+ $router->register('PATCH', '/records/*/*', array($this, 'increment'));
+ $this->service = $service;
+ $this->responder = $responder;
+ }
+
+ public function _list(ServerRequestInterface $request): ResponseInterface
+ {
+ $table = RequestUtils::getPathSegment($request, 2);
+ $params = RequestUtils::getParams($request);
+ if (!$this->service->hasTable($table)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
+ }
+ return $this->responder->success($this->service->_list($table, $params));
+ }
+
+ public function read(ServerRequestInterface $request): ResponseInterface
+ {
+ $table = RequestUtils::getPathSegment($request, 2);
+ if (!$this->service->hasTable($table)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
+ }
+ $id = RequestUtils::getPathSegment($request, 3);
+ $params = RequestUtils::getParams($request);
+ if (strpos($id, ',') !== false) {
+ $ids = explode(',', $id);
+ $result = [];
+ for ($i = 0; $i < count($ids); $i++) {
+ array_push($result, $this->service->read($table, $ids[$i], $params));
+ }
+ return $this->responder->success($result);
+ } else {
+ $response = $this->service->read($table, $id, $params);
+ if ($response === null) {
+ return $this->responder->error(ErrorCode::RECORD_NOT_FOUND, $id);
+ }
+ return $this->responder->success($response);
+ }
+ }
+
+ public function create(ServerRequestInterface $request): ResponseInterface
+ {
+ $table = RequestUtils::getPathSegment($request, 2);
+ if (!$this->service->hasTable($table)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
+ }
+ if ($this->service->getType($table) != 'table') {
+ return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
+ }
+ $record = $request->getParsedBody();
+ if ($record === null) {
+ return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, '');
+ }
+ $params = RequestUtils::getParams($request);
+ if (is_array($record)) {
+ $result = array();
+ foreach ($record as $r) {
+ $result[] = $this->service->create($table, $r, $params);
+ }
+ return $this->responder->success($result);
+ } else {
+ return $this->responder->success($this->service->create($table, $record, $params));
+ }
+ }
+
+ public function update(ServerRequestInterface $request): ResponseInterface
+ {
+ $table = RequestUtils::getPathSegment($request, 2);
+ if (!$this->service->hasTable($table)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
+ }
+ if ($this->service->getType($table) != 'table') {
+ return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
+ }
+ $id = RequestUtils::getPathSegment($request, 3);
+ $params = RequestUtils::getParams($request);
+ $record = $request->getParsedBody();
+ if ($record === null) {
+ return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, '');
+ }
+ $ids = explode(',', $id);
+ if (is_array($record)) {
+ if (count($ids) != count($record)) {
+ return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
+ }
+ $result = array();
+ for ($i = 0; $i < count($ids); $i++) {
+ $result[] = $this->service->update($table, $ids[$i], $record[$i], $params);
+ }
+ return $this->responder->success($result);
+ } else {
+ if (count($ids) != 1) {
+ return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
+ }
+ return $this->responder->success($this->service->update($table, $id, $record, $params));
+ }
+ }
+
+ public function delete(ServerRequestInterface $request): ResponseInterface
+ {
+ $table = RequestUtils::getPathSegment($request, 2);
+ if (!$this->service->hasTable($table)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
+ }
+ if ($this->service->getType($table) != 'table') {
+ return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
+ }
+ $id = RequestUtils::getPathSegment($request, 3);
+ $params = RequestUtils::getParams($request);
+ $ids = explode(',', $id);
+ if (count($ids) > 1) {
+ $result = array();
+ for ($i = 0; $i < count($ids); $i++) {
+ $result[] = $this->service->delete($table, $ids[$i], $params);
+ }
+ return $this->responder->success($result);
+ } else {
+ return $this->responder->success($this->service->delete($table, $id, $params));
+ }
+ }
+
+ public function increment(ServerRequestInterface $request): ResponseInterface
+ {
+ $table = RequestUtils::getPathSegment($request, 2);
+ if (!$this->service->hasTable($table)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
+ }
+ if ($this->service->getType($table) != 'table') {
+ return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
+ }
+ $id = RequestUtils::getPathSegment($request, 3);
+ $record = $request->getParsedBody();
+ if ($record === null) {
+ return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, '');
+ }
+ $params = RequestUtils::getParams($request);
+ $ids = explode(',', $id);
+ if (is_array($record)) {
+ if (count($ids) != count($record)) {
+ return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
+ }
+ $result = array();
+ for ($i = 0; $i < count($ids); $i++) {
+ $result[] = $this->service->increment($table, $ids[$i], $record[$i], $params);
+ }
+ return $this->responder->success($result);
+ } else {
+ if (count($ids) != 1) {
+ return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
+ }
+ return $this->responder->success($this->service->increment($table, $id, $record, $params));
+ }
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Controller/Responder.php
+namespace Tqdev\PhpCrudApi\Controller {
+
+ use Psr\Http\Message\ResponseInterface;
+
+ interface Responder
+ {
+ public function error(int $error, string $argument, $details = null): ResponseInterface;
+
+ public function success($result): ResponseInterface;
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Database/ColumnConverter.php
+namespace Tqdev\PhpCrudApi\Database {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
+
+ class ColumnConverter
+ {
+ private $driver;
+
+ public function __construct(string $driver)
+ {
+ $this->driver = $driver;
+ }
+
+ public function convertColumnValue(ReflectedColumn $column): string
+ {
+ if ($column->isBoolean()) {
+ switch ($this->driver) {
+ case 'mysql':
+ return "IFNULL(IF(?,TRUE,FALSE),NULL)";
+ case 'pgsql':
+ return "?";
+ case 'sqlsrv':
+ return "?";
+ }
+ }
+ if ($column->isBinary()) {
+ switch ($this->driver) {
+ case 'mysql':
+ return "FROM_BASE64(?)";
+ case 'pgsql':
+ return "decode(?, 'base64')";
+ case 'sqlsrv':
+ return "CONVERT(XML, ?).value('.','varbinary(max)')";
+ }
+ }
+ if ($column->isGeometry()) {
+ switch ($this->driver) {
+ case 'mysql':
+ case 'pgsql':
+ return "ST_GeomFromText(?)";
+ case 'sqlsrv':
+ return "geometry::STGeomFromText(?,0)";
+ }
+ }
+ return '?';
+ }
+
+ public function convertColumnName(ReflectedColumn $column, $value): string
+ {
+ if ($column->isBinary()) {
+ switch ($this->driver) {
+ case 'mysql':
+ return "TO_BASE64($value) as $value";
+ case 'pgsql':
+ return "encode($value::bytea, 'base64') as $value";
+ case 'sqlsrv':
+ return "CASE WHEN $value IS NULL THEN NULL ELSE (SELECT CAST($value as varbinary(max)) FOR XML PATH(''), BINARY BASE64) END as $value";
+ }
+ }
+ if ($column->isGeometry()) {
+ switch ($this->driver) {
+ case 'mysql':
+ case 'pgsql':
+ return "ST_AsText($value) as $value";
+ case 'sqlsrv':
+ return "REPLACE($value.STAsText(),' (','(') as $value";
+ }
+ }
+ return $value;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Database/ColumnsBuilder.php
+namespace Tqdev\PhpCrudApi\Database {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+
+ class ColumnsBuilder
+ {
+ private $driver;
+ private $converter;
+
+ public function __construct(string $driver)
+ {
+ $this->driver = $driver;
+ $this->converter = new ColumnConverter($driver);
+ }
+
+ public function getOffsetLimit(int $offset, int $limit): string
+ {
+ if ($limit < 0 || $offset < 0) {
+ return '';
+ }
+ switch ($this->driver) {
+ case 'mysql':
+ return " LIMIT $offset, $limit";
+ case 'pgsql':
+ return " LIMIT $limit OFFSET $offset";
+ case 'sqlsrv':
+ return " OFFSET $offset ROWS FETCH NEXT $limit ROWS ONLY";
+ case 'sqlite':
+ return " LIMIT $limit OFFSET $offset";
+ }
+ }
+
+ private function quoteColumnName(ReflectedColumn $column): string
+ {
+ return '"' . $column->getName() . '"';
+ }
+
+ public function getOrderBy(ReflectedTable $table, array $columnOrdering): string
+ {
+ if (count($columnOrdering) == 0) {
+ return '';
+ }
+ $results = array();
+ foreach ($columnOrdering as $i => list($columnName, $ordering)) {
+ $column = $table->getColumn($columnName);
+ $quotedColumnName = $this->quoteColumnName($column);
+ $results[] = $quotedColumnName . ' ' . $ordering;
+ }
+ return ' ORDER BY ' . implode(',', $results);
+ }
+
+ public function getSelect(ReflectedTable $table, array $columnNames): string
+ {
+ $results = array();
+ foreach ($columnNames as $columnName) {
+ $column = $table->getColumn($columnName);
+ $quotedColumnName = $this->quoteColumnName($column);
+ $quotedColumnName = $this->converter->convertColumnName($column, $quotedColumnName);
+ $results[] = $quotedColumnName;
+ }
+ return implode(',', $results);
+ }
+
+ public function getInsert(ReflectedTable $table, array $columnValues): string
+ {
+ $columns = array();
+ $values = array();
+ foreach ($columnValues as $columnName => $columnValue) {
+ $column = $table->getColumn($columnName);
+ $quotedColumnName = $this->quoteColumnName($column);
+ $columns[] = $quotedColumnName;
+ $columnValue = $this->converter->convertColumnValue($column);
+ $values[] = $columnValue;
+ }
+ $columnsSql = '(' . implode(',', $columns) . ')';
+ $valuesSql = '(' . implode(',', $values) . ')';
+ $outputColumn = $this->quoteColumnName($table->getPk());
+ switch ($this->driver) {
+ case 'mysql':
+ return "$columnsSql VALUES $valuesSql";
+ case 'pgsql':
+ return "$columnsSql VALUES $valuesSql RETURNING $outputColumn";
+ case 'sqlsrv':
+ return "$columnsSql OUTPUT INSERTED.$outputColumn VALUES $valuesSql";
+ case 'sqlite':
+ return "$columnsSql VALUES $valuesSql";
+ }
+ }
+
+ public function getUpdate(ReflectedTable $table, array $columnValues): string
+ {
+ $results = array();
+ foreach ($columnValues as $columnName => $columnValue) {
+ $column = $table->getColumn($columnName);
+ $quotedColumnName = $this->quoteColumnName($column);
+ $columnValue = $this->converter->convertColumnValue($column);
+ $results[] = $quotedColumnName . '=' . $columnValue;
+ }
+ return implode(',', $results);
+ }
+
+ public function getIncrement(ReflectedTable $table, array $columnValues): string
+ {
+ $results = array();
+ foreach ($columnValues as $columnName => $columnValue) {
+ if (!is_numeric($columnValue)) {
+ continue;
+ }
+ $column = $table->getColumn($columnName);
+ $quotedColumnName = $this->quoteColumnName($column);
+ $columnValue = $this->converter->convertColumnValue($column);
+ $results[] = $quotedColumnName . '=' . $quotedColumnName . '+' . $columnValue;
+ }
+ return implode(',', $results);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Database/ConditionsBuilder.php
+namespace Tqdev\PhpCrudApi\Database {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
+ use Tqdev\PhpCrudApi\Record\Condition\AndCondition;
+ use Tqdev\PhpCrudApi\Record\Condition\ColumnCondition;
+ use Tqdev\PhpCrudApi\Record\Condition\Condition;
+ use Tqdev\PhpCrudApi\Record\Condition\NoCondition;
+ use Tqdev\PhpCrudApi\Record\Condition\NotCondition;
+ use Tqdev\PhpCrudApi\Record\Condition\OrCondition;
+ use Tqdev\PhpCrudApi\Record\Condition\SpatialCondition;
+
+ class ConditionsBuilder
+ {
+ private $driver;
+
+ public function __construct(string $driver)
+ {
+ $this->driver = $driver;
+ }
+
+ private function getConditionSql(Condition $condition, array &$arguments): string
+ {
+ if ($condition instanceof AndCondition) {
+ return $this->getAndConditionSql($condition, $arguments);
+ }
+ if ($condition instanceof OrCondition) {
+ return $this->getOrConditionSql($condition, $arguments);
+ }
+ if ($condition instanceof NotCondition) {
+ return $this->getNotConditionSql($condition, $arguments);
+ }
+ if ($condition instanceof SpatialCondition) {
+ return $this->getSpatialConditionSql($condition, $arguments);
+ }
+ if ($condition instanceof ColumnCondition) {
+ return $this->getColumnConditionSql($condition, $arguments);
+ }
+ throw new \Exception('Unknown Condition: ' . get_class($condition));
+ }
+
+ private function getAndConditionSql(AndCondition $and, array &$arguments): string
+ {
+ $parts = [];
+ foreach ($and->getConditions() as $condition) {
+ $parts[] = $this->getConditionSql($condition, $arguments);
+ }
+ return '(' . implode(' AND ', $parts) . ')';
+ }
+
+ private function getOrConditionSql(OrCondition $or, array &$arguments): string
+ {
+ $parts = [];
+ foreach ($or->getConditions() as $condition) {
+ $parts[] = $this->getConditionSql($condition, $arguments);
+ }
+ return '(' . implode(' OR ', $parts) . ')';
+ }
+
+ private function getNotConditionSql(NotCondition $not, array &$arguments): string
+ {
+ $condition = $not->getCondition();
+ return '(NOT ' . $this->getConditionSql($condition, $arguments) . ')';
+ }
+
+ private function quoteColumnName(ReflectedColumn $column): string
+ {
+ return '"' . $column->getName() . '"';
+ }
+
+ private function escapeLikeValue(string $value): string
+ {
+ return addcslashes($value, '%_');
+ }
+
+ private function getColumnConditionSql(ColumnCondition $condition, array &$arguments): string
+ {
+ $column = $this->quoteColumnName($condition->getColumn());
+ $operator = $condition->getOperator();
+ $value = $condition->getValue();
+ switch ($operator) {
+ case 'cs':
+ $sql = "$column LIKE ?";
+ $arguments[] = '%' . $this->escapeLikeValue($value) . '%';
+ break;
+ case 'sw':
+ $sql = "$column LIKE ?";
+ $arguments[] = $this->escapeLikeValue($value) . '%';
+ break;
+ case 'ew':
+ $sql = "$column LIKE ?";
+ $arguments[] = '%' . $this->escapeLikeValue($value);
+ break;
+ case 'eq':
+ $sql = "$column = ?";
+ $arguments[] = $value;
+ break;
+ case 'lt':
+ $sql = "$column < ?";
+ $arguments[] = $value;
+ break;
+ case 'le':
+ $sql = "$column <= ?";
+ $arguments[] = $value;
+ break;
+ case 'ge':
+ $sql = "$column >= ?";
+ $arguments[] = $value;
+ break;
+ case 'gt':
+ $sql = "$column > ?";
+ $arguments[] = $value;
+ break;
+ case 'bt':
+ $parts = explode(',', $value, 2);
+ $count = count($parts);
+ if ($count == 2) {
+ $sql = "($column >= ? AND $column <= ?)";
+ $arguments[] = $parts[0];
+ $arguments[] = $parts[1];
+ } else {
+ $sql = "FALSE";
+ }
+ break;
+ case 'in':
+ $parts = explode(',', $value);
+ $count = count($parts);
+ if ($count > 0) {
+ $qmarks = implode(',', str_split(str_repeat('?', $count)));
+ $sql = "$column IN ($qmarks)";
+ for ($i = 0; $i < $count; $i++) {
+ $arguments[] = $parts[$i];
+ }
+ } else {
+ $sql = "FALSE";
+ }
+ break;
+ case 'is':
+ $sql = "$column IS NULL";
+ break;
+ }
+ return $sql;
+ }
+
+ private function getSpatialFunctionName(string $operator): string
+ {
+ switch ($operator) {
+ case 'co':
+ return 'ST_Contains';
+ case 'cr':
+ return 'ST_Crosses';
+ case 'di':
+ return 'ST_Disjoint';
+ case 'eq':
+ return 'ST_Equals';
+ case 'in':
+ return 'ST_Intersects';
+ case 'ov':
+ return 'ST_Overlaps';
+ case 'to':
+ return 'ST_Touches';
+ case 'wi':
+ return 'ST_Within';
+ case 'ic':
+ return 'ST_IsClosed';
+ case 'is':
+ return 'ST_IsSimple';
+ case 'iv':
+ return 'ST_IsValid';
+ }
+ }
+
+ private function hasSpatialArgument(string $operator): bool
+ {
+ return in_array($operator, ['ic', 'is', 'iv']) ? false : true;
+ }
+
+ private function getSpatialFunctionCall(string $functionName, string $column, bool $hasArgument): string
+ {
+ switch ($this->driver) {
+ case 'mysql':
+ case 'pgsql':
+ $argument = $hasArgument ? 'ST_GeomFromText(?)' : '';
+ return "$functionName($column, $argument)=TRUE";
+ case 'sqlsrv':
+ $functionName = str_replace('ST_', 'ST', $functionName);
+ $argument = $hasArgument ? 'geometry::STGeomFromText(?,0)' : '';
+ return "$column.$functionName($argument)=1";
+ case 'sqlite':
+ $argument = $hasArgument ? '?' : '0';
+ return "$functionName($column, $argument)=1";
+ }
+ }
+
+ private function getSpatialConditionSql(ColumnCondition $condition, array &$arguments): string
+ {
+ $column = $this->quoteColumnName($condition->getColumn());
+ $operator = $condition->getOperator();
+ $value = $condition->getValue();
+ $functionName = $this->getSpatialFunctionName($operator);
+ $hasArgument = $this->hasSpatialArgument($operator);
+ $sql = $this->getSpatialFunctionCall($functionName, $column, $hasArgument);
+ if ($hasArgument) {
+ $arguments[] = $value;
+ }
+ return $sql;
+ }
+
+ public function getWhereClause(Condition $condition, array &$arguments): string
+ {
+ if ($condition instanceof NoCondition) {
+ return '';
+ }
+ return ' WHERE ' . $this->getConditionSql($condition, $arguments);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Database/DataConverter.php
+namespace Tqdev\PhpCrudApi\Database {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+
+ class DataConverter
+ {
+ private $driver;
+
+ public function __construct(string $driver)
+ {
+ $this->driver = $driver;
+ }
+
+ private function convertRecordValue($conversion, $value)
+ {
+ $args = explode('|', $conversion);
+ $type = array_shift($args);
+ switch ($type) {
+ case 'boolean':
+ return $value ? true : false;
+ case 'integer':
+ return (int) $value;
+ case 'float':
+ return (float) $value;
+ case 'decimal':
+ return number_format($value, $args[0], '.', '');
+ }
+ return $value;
+ }
+
+ private function getRecordValueConversion(ReflectedColumn $column): string
+ {
+ if (in_array($this->driver, ['mysql', 'sqlsrv', 'sqlite']) && $column->isBoolean()) {
+ return 'boolean';
+ }
+ if (in_array($this->driver, ['sqlsrv', 'sqlite']) && in_array($column->getType(), ['integer', 'bigint'])) {
+ return 'integer';
+ }
+ if (in_array($this->driver, ['sqlite', 'pgsql']) && in_array($column->getType(), ['float', 'double'])) {
+ return 'float';
+ }
+ if (in_array($this->driver, ['sqlite']) && in_array($column->getType(), ['decimal'])) {
+ return 'decimal|' . $column->getScale();
+ }
+ return 'none';
+ }
+
+ public function convertRecords(ReflectedTable $table, array $columnNames, array &$records) /*: void*/
+ {
+ foreach ($columnNames as $columnName) {
+ $column = $table->getColumn($columnName);
+ $conversion = $this->getRecordValueConversion($column);
+ if ($conversion != 'none') {
+ foreach ($records as $i => $record) {
+ $value = $records[$i][$columnName];
+ if ($value === null) {
+ continue;
+ }
+ $records[$i][$columnName] = $this->convertRecordValue($conversion, $value);
+ }
+ }
+ }
+ }
+
+ private function convertInputValue($conversion, $value)
+ {
+ switch ($conversion) {
+ case 'boolean':
+ return $value ? 1 : 0;
+ case 'base64url_to_base64':
+ return str_pad(strtr($value, '-_', '+/'), ceil(strlen($value) / 4) * 4, '=', STR_PAD_RIGHT);
+ }
+ return $value;
+ }
+
+ private function getInputValueConversion(ReflectedColumn $column): string
+ {
+ if ($column->isBoolean()) {
+ return 'boolean';
+ }
+ if ($column->isBinary()) {
+ return 'base64url_to_base64';
+ }
+ return 'none';
+ }
+
+ public function convertColumnValues(ReflectedTable $table, array &$columnValues) /*: void*/
+ {
+ $columnNames = array_keys($columnValues);
+ foreach ($columnNames as $columnName) {
+ $column = $table->getColumn($columnName);
+ $conversion = $this->getInputValueConversion($column);
+ if ($conversion != 'none') {
+ $value = $columnValues[$columnName];
+ if ($value !== null) {
+ $columnValues[$columnName] = $this->convertInputValue($conversion, $value);
+ }
+ }
+ }
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Database/GenericDB.php
+namespace Tqdev\PhpCrudApi\Database {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+ use Tqdev\PhpCrudApi\Middleware\Communication\VariableStore;
+ use Tqdev\PhpCrudApi\Record\Condition\ColumnCondition;
+ use Tqdev\PhpCrudApi\Record\Condition\Condition;
+
+ class GenericDB
+ {
+ private $driver;
+ private $address;
+ private $port;
+ private $database;
+ private $tables;
+ private $username;
+ private $password;
+ private $pdo;
+ private $reflection;
+ private $definition;
+ private $conditions;
+ private $columns;
+ private $converter;
+
+ private function getDsn(): string
+ {
+ switch ($this->driver) {
+ case 'mysql':
+ return "$this->driver:host=$this->address;port=$this->port;dbname=$this->database;charset=utf8mb4";
+ case 'pgsql':
+ return "$this->driver:host=$this->address port=$this->port dbname=$this->database options='--client_encoding=UTF8'";
+ case 'sqlsrv':
+ return "$this->driver:Server=$this->address,$this->port;Database=$this->database";
+ case 'sqlite':
+ return "$this->driver:$this->address";
+ }
+ }
+
+ private function getCommands(): array
+ {
+ switch ($this->driver) {
+ case 'mysql':
+ return [
+ 'SET SESSION sql_warnings=1;',
+ 'SET NAMES utf8mb4;',
+ 'SET SESSION sql_mode = "ANSI,TRADITIONAL";',
+ ];
+ case 'pgsql':
+ return [
+ "SET NAMES 'UTF8';",
+ ];
+ case 'sqlsrv':
+ return [];
+ case 'sqlite':
+ return [
+ 'PRAGMA foreign_keys = on;',
+ ];
+ }
+ }
+
+ private function getOptions(): array
+ {
+ $options = array(
+ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
+ \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
+ );
+ switch ($this->driver) {
+ case 'mysql':
+ return $options + [
+ \PDO::ATTR_EMULATE_PREPARES => false,
+ \PDO::MYSQL_ATTR_FOUND_ROWS => true,
+ \PDO::ATTR_PERSISTENT => true,
+ ];
+ case 'pgsql':
+ return $options + [
+ \PDO::ATTR_EMULATE_PREPARES => false,
+ \PDO::ATTR_PERSISTENT => true,
+ ];
+ case 'sqlsrv':
+ return $options + [
+ \PDO::SQLSRV_ATTR_DIRECT_QUERY => false,
+ \PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE => true,
+ ];
+ case 'sqlite':
+ return $options + [];
+ }
+ }
+
+ private function initPdo(): bool
+ {
+ if ($this->pdo) {
+ $result = $this->pdo->reconstruct($this->getDsn(), $this->username, $this->password, $this->getOptions());
+ } else {
+ $this->pdo = new LazyPdo($this->getDsn(), $this->username, $this->password, $this->getOptions());
+ $result = true;
+ }
+ $commands = $this->getCommands();
+ foreach ($commands as $command) {
+ $this->pdo->addInitCommand($command);
+ }
+ $this->reflection = new GenericReflection($this->pdo, $this->driver, $this->database, $this->tables);
+ $this->definition = new GenericDefinition($this->pdo, $this->driver, $this->database, $this->tables);
+ $this->conditions = new ConditionsBuilder($this->driver);
+ $this->columns = new ColumnsBuilder($this->driver);
+ $this->converter = new DataConverter($this->driver);
+ return $result;
+ }
+
+ public function __construct(string $driver, string $address, int $port, string $database, array $tables, string $username, string $password)
+ {
+ $this->driver = $driver;
+ $this->address = $address;
+ $this->port = $port;
+ $this->database = $database;
+ $this->tables = $tables;
+ $this->username = $username;
+ $this->password = $password;
+ $this->initPdo();
+ }
+
+ public function reconstruct(string $driver, string $address, int $port, string $database, array $tables, string $username, string $password): bool
+ {
+ if ($driver) {
+ $this->driver = $driver;
+ }
+ if ($address) {
+ $this->address = $address;
+ }
+ if ($port) {
+ $this->port = $port;
+ }
+ if ($database) {
+ $this->database = $database;
+ }
+ if ($tables) {
+ $this->tables = $tables;
+ }
+ if ($username) {
+ $this->username = $username;
+ }
+ if ($password) {
+ $this->password = $password;
+ }
+ return $this->initPdo();
+ }
+
+ public function pdo(): LazyPdo
+ {
+ return $this->pdo;
+ }
+
+ public function reflection(): GenericReflection
+ {
+ return $this->reflection;
+ }
+
+ public function definition(): GenericDefinition
+ {
+ return $this->definition;
+ }
+
+ private function addMiddlewareConditions(string $tableName, Condition $condition): Condition
+ {
+ $condition1 = VariableStore::get("authorization.conditions.$tableName");
+ if ($condition1) {
+ $condition = $condition->_and($condition1);
+ }
+ $condition2 = VariableStore::get("multiTenancy.conditions.$tableName");
+ if ($condition2) {
+ $condition = $condition->_and($condition2);
+ }
+ return $condition;
+ }
+
+ public function createSingle(ReflectedTable $table, array $columnValues) /*: ?String*/
+ {
+ $this->converter->convertColumnValues($table, $columnValues);
+ $insertColumns = $this->columns->getInsert($table, $columnValues);
+ $tableName = $table->getName();
+ $pkName = $table->getPk()->getName();
+ $parameters = array_values($columnValues);
+ $sql = 'INSERT INTO "' . $tableName . '" ' . $insertColumns;
+ $stmt = $this->query($sql, $parameters);
+ // return primary key value if specified in the input
+ if (isset($columnValues[$pkName])) {
+ return $columnValues[$pkName];
+ }
+ // work around missing "returning" or "output" in mysql
+ switch ($this->driver) {
+ case 'mysql':
+ $stmt = $this->query('SELECT LAST_INSERT_ID()', []);
+ break;
+ case 'sqlite':
+ $stmt = $this->query('SELECT LAST_INSERT_ROWID()', []);
+ break;
+ }
+ $pkValue = $stmt->fetchColumn(0);
+ if ($this->driver == 'sqlsrv' && $table->getPk()->getType() == 'bigint') {
+ return (int) $pkValue;
+ }
+ if ($this->driver == 'sqlite' && in_array($table->getPk()->getType(), ['integer', 'bigint'])) {
+ return (int) $pkValue;
+ }
+ return $pkValue;
+ }
+
+ public function selectSingle(ReflectedTable $table, array $columnNames, string $id) /*: ?array*/
+ {
+ $selectColumns = $this->columns->getSelect($table, $columnNames);
+ $tableName = $table->getName();
+ $condition = new ColumnCondition($table->getPk(), 'eq', $id);
+ $condition = $this->addMiddlewareConditions($tableName, $condition);
+ $parameters = array();
+ $whereClause = $this->conditions->getWhereClause($condition, $parameters);
+ $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '" ' . $whereClause;
+ $stmt = $this->query($sql, $parameters);
+ $record = $stmt->fetch() ?: null;
+ if ($record === null) {
+ return null;
+ }
+ $records = array($record);
+ $this->converter->convertRecords($table, $columnNames, $records);
+ return $records[0];
+ }
+
+ public function selectMultiple(ReflectedTable $table, array $columnNames, array $ids): array
+ {
+ if (count($ids) == 0) {
+ return [];
+ }
+ $selectColumns = $this->columns->getSelect($table, $columnNames);
+ $tableName = $table->getName();
+ $condition = new ColumnCondition($table->getPk(), 'in', implode(',', $ids));
+ $condition = $this->addMiddlewareConditions($tableName, $condition);
+ $parameters = array();
+ $whereClause = $this->conditions->getWhereClause($condition, $parameters);
+ $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '" ' . $whereClause;
+ $stmt = $this->query($sql, $parameters);
+ $records = $stmt->fetchAll();
+ $this->converter->convertRecords($table, $columnNames, $records);
+ return $records;
+ }
+
+ public function selectCount(ReflectedTable $table, Condition $condition): int
+ {
+ $tableName = $table->getName();
+ $condition = $this->addMiddlewareConditions($tableName, $condition);
+ $parameters = array();
+ $whereClause = $this->conditions->getWhereClause($condition, $parameters);
+ $sql = 'SELECT COUNT(*) FROM "' . $tableName . '"' . $whereClause;
+ $stmt = $this->query($sql, $parameters);
+ return $stmt->fetchColumn(0);
+ }
+
+ public function selectAll(ReflectedTable $table, array $columnNames, Condition $condition, array $columnOrdering, int $offset, int $limit): array
+ {
+ if ($limit == 0) {
+ return array();
+ }
+ $selectColumns = $this->columns->getSelect($table, $columnNames);
+ $tableName = $table->getName();
+ $condition = $this->addMiddlewareConditions($tableName, $condition);
+ $parameters = array();
+ $whereClause = $this->conditions->getWhereClause($condition, $parameters);
+ $orderBy = $this->columns->getOrderBy($table, $columnOrdering);
+ $offsetLimit = $this->columns->getOffsetLimit($offset, $limit);
+ $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '"' . $whereClause . $orderBy . $offsetLimit;
+ $stmt = $this->query($sql, $parameters);
+ $records = $stmt->fetchAll();
+ $this->converter->convertRecords($table, $columnNames, $records);
+ return $records;
+ }
+
+ public function updateSingle(ReflectedTable $table, array $columnValues, string $id)
+ {
+ if (count($columnValues) == 0) {
+ return 0;
+ }
+ $this->converter->convertColumnValues($table, $columnValues);
+ $updateColumns = $this->columns->getUpdate($table, $columnValues);
+ $tableName = $table->getName();
+ $condition = new ColumnCondition($table->getPk(), 'eq', $id);
+ $condition = $this->addMiddlewareConditions($tableName, $condition);
+ $parameters = array_values($columnValues);
+ $whereClause = $this->conditions->getWhereClause($condition, $parameters);
+ $sql = 'UPDATE "' . $tableName . '" SET ' . $updateColumns . $whereClause;
+ $stmt = $this->query($sql, $parameters);
+ return $stmt->rowCount();
+ }
+
+ public function deleteSingle(ReflectedTable $table, string $id)
+ {
+ $tableName = $table->getName();
+ $condition = new ColumnCondition($table->getPk(), 'eq', $id);
+ $condition = $this->addMiddlewareConditions($tableName, $condition);
+ $parameters = array();
+ $whereClause = $this->conditions->getWhereClause($condition, $parameters);
+ $sql = 'DELETE FROM "' . $tableName . '" ' . $whereClause;
+ $stmt = $this->query($sql, $parameters);
+ return $stmt->rowCount();
+ }
+
+ public function incrementSingle(ReflectedTable $table, array $columnValues, string $id)
+ {
+ if (count($columnValues) == 0) {
+ return 0;
+ }
+ $this->converter->convertColumnValues($table, $columnValues);
+ $updateColumns = $this->columns->getIncrement($table, $columnValues);
+ $tableName = $table->getName();
+ $condition = new ColumnCondition($table->getPk(), 'eq', $id);
+ $condition = $this->addMiddlewareConditions($tableName, $condition);
+ $parameters = array_values($columnValues);
+ $whereClause = $this->conditions->getWhereClause($condition, $parameters);
+ $sql = 'UPDATE "' . $tableName . '" SET ' . $updateColumns . $whereClause;
+ $stmt = $this->query($sql, $parameters);
+ return $stmt->rowCount();
+ }
+
+ private function query(string $sql, array $parameters): \PDOStatement
+ {
+ $stmt = $this->pdo->prepare($sql);
+ //echo "- $sql -- " . json_encode($parameters, JSON_UNESCAPED_UNICODE) . "\n";
+ $stmt->execute($parameters);
+ return $stmt;
+ }
+
+ public function getCacheKey(): string
+ {
+ return md5(json_encode([
+ $this->driver,
+ $this->address,
+ $this->port,
+ $this->database,
+ $this->tables,
+ $this->username
+ ]));
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Database/GenericDefinition.php
+namespace Tqdev\PhpCrudApi\Database {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+ use Tqdev\PhpCrudApi\Database\LazyPdo;
+
+ class GenericDefinition
+ {
+ private $pdo;
+ private $driver;
+ private $database;
+ private $typeConverter;
+ private $reflection;
+
+ public function __construct(LazyPdo $pdo, string $driver, string $database, array $tables)
+ {
+ $this->pdo = $pdo;
+ $this->driver = $driver;
+ $this->database = $database;
+ $this->typeConverter = new TypeConverter($driver);
+ $this->reflection = new GenericReflection($pdo, $driver, $database, $tables);
+ }
+
+ private function quote(string $identifier): string
+ {
+ return '"' . str_replace('"', '', $identifier) . '"';
+ }
+
+ public function getColumnType(ReflectedColumn $column, bool $update): string
+ {
+ if ($this->driver == 'pgsql' && !$update && $column->getPk() && $this->canAutoIncrement($column)) {
+ return 'serial';
+ }
+ $type = $this->typeConverter->fromJdbc($column->getType());
+ if ($column->hasPrecision() && $column->hasScale()) {
+ $size = '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
+ } elseif ($column->hasPrecision()) {
+ $size = '(' . $column->getPrecision() . ')';
+ } elseif ($column->hasLength()) {
+ $size = '(' . $column->getLength() . ')';
+ } else {
+ $size = '';
+ }
+ $null = $this->getColumnNullType($column, $update);
+ $auto = $this->getColumnAutoIncrement($column, $update);
+ return $type . $size . $null . $auto;
+ }
+
+ private function getPrimaryKey(string $tableName): string
+ {
+ $pks = $this->reflection->getTablePrimaryKeys($tableName);
+ if (count($pks) == 1) {
+ return $pks[0];
+ }
+ return "";
+ }
+
+ private function canAutoIncrement(ReflectedColumn $column): bool
+ {
+ return in_array($column->getType(), ['integer', 'bigint']);
+ }
+
+ private function getColumnAutoIncrement(ReflectedColumn $column, bool $update): string
+ {
+ if (!$this->canAutoIncrement($column)) {
+ return '';
+ }
+ switch ($this->driver) {
+ case 'mysql':
+ return $column->getPk() ? ' AUTO_INCREMENT' : '';
+ case 'pgsql':
+ case 'sqlsrv':
+ return $column->getPk() ? ' IDENTITY(1,1)' : '';
+ case 'sqlite':
+ return $column->getPk() ? ' AUTOINCREMENT' : '';
+ }
+ }
+
+ private function getColumnNullType(ReflectedColumn $column, bool $update): string
+ {
+ if ($this->driver == 'pgsql' && $update) {
+ return '';
+ }
+ return $column->getNullable() ? ' NULL' : ' NOT NULL';
+ }
+
+ private function getTableRenameSQL(string $tableName, string $newTableName): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($newTableName);
+
+ switch ($this->driver) {
+ case 'mysql':
+ return "RENAME TABLE $p1 TO $p2";
+ case 'pgsql':
+ return "ALTER TABLE $p1 RENAME TO $p2";
+ case 'sqlsrv':
+ return "EXEC sp_rename $p1, $p2";
+ case 'sqlite':
+ return "ALTER TABLE $p1 RENAME TO $p2";
+ }
+ }
+
+ private function getColumnRenameSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($columnName);
+ $p3 = $this->quote($newColumn->getName());
+
+ switch ($this->driver) {
+ case 'mysql':
+ $p4 = $this->getColumnType($newColumn, true);
+ return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
+ case 'pgsql':
+ return "ALTER TABLE $p1 RENAME COLUMN $p2 TO $p3";
+ case 'sqlsrv':
+ $p4 = $this->quote($tableName . '.' . $columnName);
+ return "EXEC sp_rename $p4, $p3, 'COLUMN'";
+ case 'sqlite':
+ return "ALTER TABLE $p1 RENAME COLUMN $p2 TO $p3";
+ }
+ }
+
+ private function getColumnRetypeSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($columnName);
+ $p3 = $this->quote($newColumn->getName());
+ $p4 = $this->getColumnType($newColumn, true);
+
+ switch ($this->driver) {
+ case 'mysql':
+ return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
+ case 'pgsql':
+ return "ALTER TABLE $p1 ALTER COLUMN $p3 TYPE $p4";
+ case 'sqlsrv':
+ return "ALTER TABLE $p1 ALTER COLUMN $p3 $p4";
+ }
+ }
+
+ private function getSetColumnNullableSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($columnName);
+ $p3 = $this->quote($newColumn->getName());
+ $p4 = $this->getColumnType($newColumn, true);
+
+ switch ($this->driver) {
+ case 'mysql':
+ return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
+ case 'pgsql':
+ $p5 = $newColumn->getNullable() ? 'DROP NOT NULL' : 'SET NOT NULL';
+ return "ALTER TABLE $p1 ALTER COLUMN $p2 $p5";
+ case 'sqlsrv':
+ return "ALTER TABLE $p1 ALTER COLUMN $p2 $p4";
+ }
+ }
+
+ private function getSetColumnPkConstraintSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($columnName);
+ $p3 = $this->quote($tableName . '_pkey');
+
+ switch ($this->driver) {
+ case 'mysql':
+ $p4 = $newColumn->getPk() ? "ADD PRIMARY KEY ($p2)" : 'DROP PRIMARY KEY';
+ return "ALTER TABLE $p1 $p4";
+ case 'pgsql':
+ case 'sqlsrv':
+ $p4 = $newColumn->getPk() ? "ADD CONSTRAINT $p3 PRIMARY KEY ($p2)" : "DROP CONSTRAINT $p3";
+ return "ALTER TABLE $p1 $p4";
+ }
+ }
+
+ private function getSetColumnPkSequenceSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($columnName);
+ $p3 = $this->quote($tableName . '_' . $columnName . '_seq');
+
+ switch ($this->driver) {
+ case 'mysql':
+ return "select 1";
+ case 'pgsql':
+ return $newColumn->getPk() ? "CREATE SEQUENCE $p3 OWNED BY $p1.$p2" : "DROP SEQUENCE $p3";
+ case 'sqlsrv':
+ return $newColumn->getPk() ? "CREATE SEQUENCE $p3" : "DROP SEQUENCE $p3";
+ }
+ }
+
+ private function getSetColumnPkSequenceStartSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($columnName);
+
+ switch ($this->driver) {
+ case 'mysql':
+ return "select 1";
+ case 'pgsql':
+ $p3 = $this->pdo->quote($tableName . '_' . $columnName . '_seq');
+ return "SELECT setval($p3, (SELECT max($p2)+1 FROM $p1));";
+ case 'sqlsrv':
+ $p3 = $this->quote($tableName . '_' . $columnName . '_seq');
+ $p4 = $this->pdo->query("SELECT max($p2)+1 FROM $p1")->fetchColumn();
+ return "ALTER SEQUENCE $p3 RESTART WITH $p4";
+ }
+ }
+
+ private function getSetColumnPkDefaultSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($columnName);
+
+ switch ($this->driver) {
+ case 'mysql':
+ $p3 = $this->quote($newColumn->getName());
+ $p4 = $this->getColumnType($newColumn, true);
+ return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
+ case 'pgsql':
+ if ($newColumn->getPk()) {
+ $p3 = $this->pdo->quote($tableName . '_' . $columnName . '_seq');
+ $p4 = "SET DEFAULT nextval($p3)";
+ } else {
+ $p4 = 'DROP DEFAULT';
+ }
+ return "ALTER TABLE $p1 ALTER COLUMN $p2 $p4";
+ case 'sqlsrv':
+ $p3 = $this->quote($tableName . '_' . $columnName . '_seq');
+ $p4 = $this->quote($tableName . '_' . $columnName . '_def');
+ if ($newColumn->getPk()) {
+ return "ALTER TABLE $p1 ADD CONSTRAINT $p4 DEFAULT NEXT VALUE FOR $p3 FOR $p2";
+ } else {
+ return "ALTER TABLE $p1 DROP CONSTRAINT $p4";
+ }
+ }
+ }
+
+ private function getAddColumnFkConstraintSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($columnName);
+ $p3 = $this->quote($tableName . '_' . $columnName . '_fkey');
+ $p4 = $this->quote($newColumn->getFk());
+ $p5 = $this->quote($this->getPrimaryKey($newColumn->getFk()));
+
+ return "ALTER TABLE $p1 ADD CONSTRAINT $p3 FOREIGN KEY ($p2) REFERENCES $p4 ($p5)";
+ }
+
+ private function getRemoveColumnFkConstraintSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($tableName . '_' . $columnName . '_fkey');
+
+ switch ($this->driver) {
+ case 'mysql':
+ return "ALTER TABLE $p1 DROP FOREIGN KEY $p2";
+ case 'pgsql':
+ case 'sqlsrv':
+ return "ALTER TABLE $p1 DROP CONSTRAINT $p2";
+ }
+ }
+
+ private function getAddTableSQL(ReflectedTable $newTable): string
+ {
+ $tableName = $newTable->getName();
+ $p1 = $this->quote($tableName);
+ $fields = [];
+ $constraints = [];
+ foreach ($newTable->getColumnNames() as $columnName) {
+ $pkColumn = $this->getPrimaryKey($tableName);
+ $newColumn = $newTable->getColumn($columnName);
+ $f1 = $this->quote($columnName);
+ $f2 = $this->getColumnType($newColumn, false);
+ $f3 = $this->quote($tableName . '_' . $columnName . '_fkey');
+ $f4 = $this->quote($newColumn->getFk());
+ $f5 = $this->quote($this->getPrimaryKey($newColumn->getFk()));
+ $f6 = $this->quote($tableName . '_' . $pkColumn . '_pkey');
+ if ($this->driver == 'sqlite') {
+ if ($newColumn->getPk()) {
+ $f2 = str_replace('NULL', 'NULL PRIMARY KEY', $f2);
+ }
+ $fields[] = "$f1 $f2";
+ if ($newColumn->getFk()) {
+ $constraints[] = "FOREIGN KEY ($f1) REFERENCES $f4 ($f5)";
+ }
+ } else {
+ $fields[] = "$f1 $f2";
+ if ($newColumn->getPk()) {
+ $constraints[] = "CONSTRAINT $f6 PRIMARY KEY ($f1)";
+ }
+ if ($newColumn->getFk()) {
+ $constraints[] = "CONSTRAINT $f3 FOREIGN KEY ($f1) REFERENCES $f4 ($f5)";
+ }
+ }
+ }
+ $p2 = implode(',', array_merge($fields, $constraints));
+
+ return "CREATE TABLE $p1 ($p2);";
+ }
+
+ private function getAddColumnSQL(string $tableName, ReflectedColumn $newColumn): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($newColumn->getName());
+ $p3 = $this->getColumnType($newColumn, false);
+
+ switch ($this->driver) {
+ case 'mysql':
+ case 'pgsql':
+ return "ALTER TABLE $p1 ADD COLUMN $p2 $p3";
+ case 'sqlsrv':
+ return "ALTER TABLE $p1 ADD $p2 $p3";
+ case 'sqlite':
+ return "ALTER TABLE $p1 ADD COLUMN $p2 $p3";
+ }
+ }
+
+ private function getRemoveTableSQL(string $tableName): string
+ {
+ $p1 = $this->quote($tableName);
+
+ switch ($this->driver) {
+ case 'mysql':
+ case 'pgsql':
+ return "DROP TABLE $p1 CASCADE;";
+ case 'sqlsrv':
+ return "DROP TABLE $p1;";
+ case 'sqlite':
+ return "DROP TABLE $p1;";
+ }
+ }
+
+ private function getRemoveColumnSQL(string $tableName, string $columnName): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($columnName);
+
+ switch ($this->driver) {
+ case 'mysql':
+ case 'pgsql':
+ return "ALTER TABLE $p1 DROP COLUMN $p2 CASCADE;";
+ case 'sqlsrv':
+ return "ALTER TABLE $p1 DROP COLUMN $p2;";
+ case 'sqlite':
+ return "ALTER TABLE $p1 DROP COLUMN $p2;";
+ }
+ }
+
+ public function renameTable(string $tableName, string $newTableName)
+ {
+ $sql = $this->getTableRenameSQL($tableName, $newTableName);
+ return $this->query($sql, []);
+ }
+
+ public function renameColumn(string $tableName, string $columnName, ReflectedColumn $newColumn)
+ {
+ $sql = $this->getColumnRenameSQL($tableName, $columnName, $newColumn);
+ return $this->query($sql, []);
+ }
+
+ public function retypeColumn(string $tableName, string $columnName, ReflectedColumn $newColumn)
+ {
+ $sql = $this->getColumnRetypeSQL($tableName, $columnName, $newColumn);
+ return $this->query($sql, []);
+ }
+
+ public function setColumnNullable(string $tableName, string $columnName, ReflectedColumn $newColumn)
+ {
+ $sql = $this->getSetColumnNullableSQL($tableName, $columnName, $newColumn);
+ return $this->query($sql, []);
+ }
+
+ public function addColumnPrimaryKey(string $tableName, string $columnName, ReflectedColumn $newColumn)
+ {
+ $sql = $this->getSetColumnPkConstraintSQL($tableName, $columnName, $newColumn);
+ $this->query($sql, []);
+ if ($this->canAutoIncrement($newColumn)) {
+ $sql = $this->getSetColumnPkSequenceSQL($tableName, $columnName, $newColumn);
+ $this->query($sql, []);
+ $sql = $this->getSetColumnPkSequenceStartSQL($tableName, $columnName, $newColumn);
+ $this->query($sql, []);
+ $sql = $this->getSetColumnPkDefaultSQL($tableName, $columnName, $newColumn);
+ $this->query($sql, []);
+ }
+ return true;
+ }
+
+ public function removeColumnPrimaryKey(string $tableName, string $columnName, ReflectedColumn $newColumn)
+ {
+ if ($this->canAutoIncrement($newColumn)) {
+ $sql = $this->getSetColumnPkDefaultSQL($tableName, $columnName, $newColumn);
+ $this->query($sql, []);
+ $sql = $this->getSetColumnPkSequenceSQL($tableName, $columnName, $newColumn);
+ $this->query($sql, []);
+ }
+ $sql = $this->getSetColumnPkConstraintSQL($tableName, $columnName, $newColumn);
+ $this->query($sql, []);
+ return true;
+ }
+
+ public function addColumnForeignKey(string $tableName, string $columnName, ReflectedColumn $newColumn)
+ {
+ $sql = $this->getAddColumnFkConstraintSQL($tableName, $columnName, $newColumn);
+ return $this->query($sql, []);
+ }
+
+ public function removeColumnForeignKey(string $tableName, string $columnName, ReflectedColumn $newColumn)
+ {
+ $sql = $this->getRemoveColumnFkConstraintSQL($tableName, $columnName, $newColumn);
+ return $this->query($sql, []);
+ }
+
+ public function addTable(ReflectedTable $newTable)
+ {
+ $sql = $this->getAddTableSQL($newTable);
+ return $this->query($sql, []);
+ }
+
+ public function addColumn(string $tableName, ReflectedColumn $newColumn)
+ {
+ $sql = $this->getAddColumnSQL($tableName, $newColumn);
+ return $this->query($sql, []);
+ }
+
+ public function removeTable(string $tableName)
+ {
+ $sql = $this->getRemoveTableSQL($tableName);
+ return $this->query($sql, []);
+ }
+
+ public function removeColumn(string $tableName, string $columnName)
+ {
+ $sql = $this->getRemoveColumnSQL($tableName, $columnName);
+ return $this->query($sql, []);
+ }
+
+ private function query(string $sql, array $arguments): bool
+ {
+ $stmt = $this->pdo->prepare($sql);
+ // echo "- $sql -- " . json_encode($arguments) . "\n";
+ return $stmt->execute($arguments);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Database/GenericReflection.php
+namespace Tqdev\PhpCrudApi\Database {
+
+ use Tqdev\PhpCrudApi\Database\LazyPdo;
+
+ class GenericReflection
+ {
+ private $pdo;
+ private $driver;
+ private $database;
+ private $tables;
+ private $typeConverter;
+
+ public function __construct(LazyPdo $pdo, string $driver, string $database, array $tables)
+ {
+ $this->pdo = $pdo;
+ $this->driver = $driver;
+ $this->database = $database;
+ $this->tables = $tables;
+ $this->typeConverter = new TypeConverter($driver);
+ }
+
+ public function getIgnoredTables(): array
+ {
+ switch ($this->driver) {
+ case 'mysql':
+ return [];
+ case 'pgsql':
+ return ['spatial_ref_sys', 'raster_columns', 'raster_overviews', 'geography_columns', 'geometry_columns'];
+ case 'sqlsrv':
+ return [];
+ case 'sqlite':
+ return [];
+ }
+ }
+
+ private function getTablesSQL(): string
+ {
+ switch ($this->driver) {
+ case 'mysql':
+ return 'SELECT "TABLE_NAME", "TABLE_TYPE" FROM "INFORMATION_SCHEMA"."TABLES" WHERE "TABLE_TYPE" IN (\'BASE TABLE\' , \'VIEW\') AND "TABLE_SCHEMA" = ? ORDER BY BINARY "TABLE_NAME"';
+ case 'pgsql':
+ return 'SELECT c.relname as "TABLE_NAME", c.relkind as "TABLE_TYPE" FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind IN (\'r\', \'v\') AND n.nspname <> \'pg_catalog\' AND n.nspname <> \'information_schema\' AND n.nspname !~ \'^pg_toast\' AND pg_catalog.pg_table_is_visible(c.oid) AND \'\' <> ? ORDER BY "TABLE_NAME";';
+ case 'sqlsrv':
+ return 'SELECT o.name as "TABLE_NAME", o.xtype as "TABLE_TYPE" FROM sysobjects o WHERE o.xtype IN (\'U\', \'V\') ORDER BY "TABLE_NAME"';
+ case 'sqlite':
+ return 'SELECT t.name as "TABLE_NAME", t.type as "TABLE_TYPE" FROM sqlite_master t WHERE t.type IN (\'table\', \'view\') AND \'\' <> ? ORDER BY "TABLE_NAME"';
+ }
+ }
+
+ private function getTableColumnsSQL(): string
+ {
+ switch ($this->driver) {
+ case 'mysql':
+ return 'SELECT "COLUMN_NAME", "IS_NULLABLE", "DATA_TYPE", "CHARACTER_MAXIMUM_LENGTH" as "CHARACTER_MAXIMUM_LENGTH", "NUMERIC_PRECISION", "NUMERIC_SCALE", "COLUMN_TYPE" FROM "INFORMATION_SCHEMA"."COLUMNS" WHERE "TABLE_NAME" = ? AND "TABLE_SCHEMA" = ? ORDER BY "ORDINAL_POSITION"';
+ case 'pgsql':
+ return 'SELECT a.attname AS "COLUMN_NAME", case when a.attnotnull then \'NO\' else \'YES\' end as "IS_NULLABLE", pg_catalog.format_type(a.atttypid, -1) as "DATA_TYPE", case when a.atttypmod < 0 then NULL else a.atttypmod-4 end as "CHARACTER_MAXIMUM_LENGTH", case when a.atttypid != 1700 then NULL else ((a.atttypmod - 4) >> 16) & 65535 end as "NUMERIC_PRECISION", case when a.atttypid != 1700 then NULL else (a.atttypmod - 4) & 65535 end as "NUMERIC_SCALE", \'\' AS "COLUMN_TYPE" FROM pg_attribute a JOIN pg_class pgc ON pgc.oid = a.attrelid WHERE pgc.relname = ? AND \'\' <> ? AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum;';
+ case 'sqlsrv':
+ return 'SELECT c.name AS "COLUMN_NAME", c.is_nullable AS "IS_NULLABLE", t.Name AS "DATA_TYPE", (c.max_length/2) AS "CHARACTER_MAXIMUM_LENGTH", c.precision AS "NUMERIC_PRECISION", c.scale AS "NUMERIC_SCALE", \'\' AS "COLUMN_TYPE" FROM sys.columns c INNER JOIN sys.types t ON c.user_type_id = t.user_type_id WHERE c.object_id = OBJECT_ID(?) AND \'\' <> ? ORDER BY c.column_id';
+ case 'sqlite':
+ return 'SELECT "name" AS "COLUMN_NAME", case when "notnull"==1 then \'no\' else \'yes\' end as "IS_NULLABLE", lower("type") AS "DATA_TYPE", 2147483647 AS "CHARACTER_MAXIMUM_LENGTH", 0 AS "NUMERIC_PRECISION", 0 AS "NUMERIC_SCALE", \'\' AS "COLUMN_TYPE" FROM pragma_table_info(?) WHERE \'\' <> ? ORDER BY "cid"';
+ }
+ }
+
+ private function getTablePrimaryKeysSQL(): string
+ {
+ switch ($this->driver) {
+ case 'mysql':
+ return 'SELECT "COLUMN_NAME" FROM "INFORMATION_SCHEMA"."KEY_COLUMN_USAGE" WHERE "CONSTRAINT_NAME" = \'PRIMARY\' AND "TABLE_NAME" = ? AND "TABLE_SCHEMA" = ?';
+ case 'pgsql':
+ return 'SELECT a.attname AS "COLUMN_NAME" FROM pg_attribute a JOIN pg_constraint c ON (c.conrelid, c.conkey[1]) = (a.attrelid, a.attnum) JOIN pg_class pgc ON pgc.oid = a.attrelid WHERE pgc.relname = ? AND \'\' <> ? AND c.contype = \'p\'';
+ case 'sqlsrv':
+ return 'SELECT c.NAME as "COLUMN_NAME" FROM sys.key_constraints kc inner join sys.objects t on t.object_id = kc.parent_object_id INNER JOIN sys.index_columns ic ON kc.parent_object_id = ic.object_id and kc.unique_index_id = ic.index_id INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id WHERE kc.type = \'PK\' and t.object_id = OBJECT_ID(?) and \'\' <> ?';
+ case 'sqlite':
+ return 'SELECT "name" as "COLUMN_NAME" FROM pragma_table_info(?) WHERE "pk"=1 AND \'\' <> ?';
+ }
+ }
+
+ private function getTableForeignKeysSQL(): string
+ {
+ switch ($this->driver) {
+ case 'mysql':
+ return 'SELECT "COLUMN_NAME", "REFERENCED_TABLE_NAME" FROM "INFORMATION_SCHEMA"."KEY_COLUMN_USAGE" WHERE "REFERENCED_TABLE_NAME" IS NOT NULL AND "TABLE_NAME" = ? AND "TABLE_SCHEMA" = ?';
+ case 'pgsql':
+ return 'SELECT a.attname AS "COLUMN_NAME", c.confrelid::regclass::text AS "REFERENCED_TABLE_NAME" FROM pg_attribute a JOIN pg_constraint c ON (c.conrelid, c.conkey[1]) = (a.attrelid, a.attnum) JOIN pg_class pgc ON pgc.oid = a.attrelid WHERE pgc.relname = ? AND \'\' <> ? AND c.contype = \'f\'';
+ case 'sqlsrv':
+ return 'SELECT COL_NAME(fc.parent_object_id, fc.parent_column_id) AS "COLUMN_NAME", OBJECT_NAME (f.referenced_object_id) AS "REFERENCED_TABLE_NAME" FROM sys.foreign_keys AS f INNER JOIN sys.foreign_key_columns AS fc ON f.OBJECT_ID = fc.constraint_object_id WHERE f.parent_object_id = OBJECT_ID(?) and \'\' <> ?';
+ case 'sqlite':
+ return 'SELECT "from" AS "COLUMN_NAME", "table" AS "REFERENCED_TABLE_NAME" FROM pragma_foreign_key_list(?) WHERE \'\' <> ?';
+ }
+ }
+
+ public function getDatabaseName(): string
+ {
+ return $this->database;
+ }
+
+ public function getTables(): array
+ {
+ $sql = $this->getTablesSQL();
+ $results = $this->query($sql, [$this->database]);
+ $tables = $this->tables;
+ $results = array_filter($results, function ($v) use ($tables) {
+ return !$tables || in_array($v['TABLE_NAME'], $tables);
+ });
+ foreach ($results as &$result) {
+ $map = [];
+ switch ($this->driver) {
+ case 'mysql':
+ $map = ['BASE TABLE' => 'table', 'VIEW' => 'view'];
+ break;
+ case 'pgsql':
+ $map = ['r' => 'table', 'v' => 'view'];
+ break;
+ case 'sqlsrv':
+ $map = ['U' => 'table', 'V' => 'view'];
+ break;
+ case 'sqlite':
+ $map = ['table' => 'table', 'view' => 'view'];
+ break;
+ }
+ $result['TABLE_TYPE'] = $map[trim($result['TABLE_TYPE'])];
+ }
+ return $results;
+ }
+
+ public function getTableColumns(string $tableName, string $type): array
+ {
+ $sql = $this->getTableColumnsSQL();
+ $results = $this->query($sql, [$tableName, $this->database]);
+ if ($type == 'view') {
+ foreach ($results as &$result) {
+ $result['IS_NULLABLE'] = false;
+ }
+ }
+ if ($this->driver == 'mysql') {
+ foreach ($results as &$result) {
+ // mysql does not properly reflect display width of types
+ preg_match('|([a-z]+)(\(([0-9]+)(,([0-9]+))?\))?|', $result['DATA_TYPE'], $matches);
+ $result['DATA_TYPE'] = $matches[1];
+ if (!$result['CHARACTER_MAXIMUM_LENGTH']) {
+ if (isset($matches[3])) {
+ $result['NUMERIC_PRECISION'] = $matches[3];
+ }
+ if (isset($matches[5])) {
+ $result['NUMERIC_SCALE'] = $matches[5];
+ }
+ }
+ }
+ }
+ if ($this->driver == 'sqlite') {
+ foreach ($results as &$result) {
+ // sqlite does not properly reflect display width of types
+ preg_match('|([a-z]+)(\(([0-9]+)(,([0-9]+))?\))?|', $result['DATA_TYPE'], $matches);
+ if (isset($matches[1])) {
+ $result['DATA_TYPE'] = $matches[1];
+ } else {
+ $result['DATA_TYPE'] = 'integer';
+ }
+ if (isset($matches[5])) {
+ $result['NUMERIC_PRECISION'] = $matches[3];
+ $result['NUMERIC_SCALE'] = $matches[5];
+ } else if (isset($matches[3])) {
+ $result['CHARACTER_MAXIMUM_LENGTH'] = $matches[3];
+ }
+ }
+ }
+ return $results;
+ }
+
+ public function getTablePrimaryKeys(string $tableName): array
+ {
+ $sql = $this->getTablePrimaryKeysSQL();
+ $results = $this->query($sql, [$tableName, $this->database]);
+ $primaryKeys = [];
+ foreach ($results as $result) {
+ $primaryKeys[] = $result['COLUMN_NAME'];
+ }
+ return $primaryKeys;
+ }
+
+ public function getTableForeignKeys(string $tableName): array
+ {
+ $sql = $this->getTableForeignKeysSQL();
+ $results = $this->query($sql, [$tableName, $this->database]);
+ $foreignKeys = [];
+ foreach ($results as $result) {
+ $foreignKeys[$result['COLUMN_NAME']] = $result['REFERENCED_TABLE_NAME'];
+ }
+ return $foreignKeys;
+ }
+
+ public function toJdbcType(string $type, string $size): string
+ {
+ return $this->typeConverter->toJdbc($type, $size);
+ }
+
+ private function query(string $sql, array $parameters): array
+ {
+ $stmt = $this->pdo->prepare($sql);
+ //echo "- $sql -- " . json_encode($parameters, JSON_UNESCAPED_UNICODE) . "\n";
+ $stmt->execute($parameters);
+ return $stmt->fetchAll();
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Database/LazyPdo.php
+namespace Tqdev\PhpCrudApi\Database {
+
+ class LazyPdo extends \PDO
+ {
+ private $dsn;
+ private $user;
+ private $password;
+ private $options;
+ private $commands;
+
+ private $pdo = null;
+
+ public function __construct(string $dsn, /*?string*/ $user = null, /*?string*/ $password = null, array $options = array())
+ {
+ $this->dsn = $dsn;
+ $this->user = $user;
+ $this->password = $password;
+ $this->options = $options;
+ $this->commands = array();
+ // explicitly NOT calling super::__construct
+ }
+
+ public function addInitCommand(string $command)/*: void*/
+ {
+ $this->commands[] = $command;
+ }
+
+ private function pdo()
+ {
+ if (!$this->pdo) {
+ $this->pdo = new \PDO($this->dsn, $this->user, $this->password, $this->options);
+ foreach ($this->commands as $command) {
+ $this->pdo->query($command);
+ }
+ }
+ return $this->pdo;
+ }
+
+ public function reconstruct(string $dsn, /*?string*/ $user = null, /*?string*/ $password = null, array $options = array()): bool
+ {
+ $this->dsn = $dsn;
+ $this->user = $user;
+ $this->password = $password;
+ $this->options = $options;
+ $this->commands = array();
+ if ($this->pdo) {
+ $this->pdo = null;
+ return true;
+ }
+ return false;
+ }
+
+ public function inTransaction(): bool
+ {
+ // Do not call parent method if there is no pdo object
+ return $this->pdo && parent::inTransaction();
+ }
+
+ public function setAttribute($attribute, $value): bool
+ {
+ if ($this->pdo) {
+ return $this->pdo()->setAttribute($attribute, $value);
+ }
+ $this->options[$attribute] = $value;
+ return true;
+ }
+
+ public function getAttribute($attribute): mixed
+ {
+ return $this->pdo()->getAttribute($attribute);
+ }
+
+ public function beginTransaction(): bool
+ {
+ return $this->pdo()->beginTransaction();
+ }
+
+ public function commit(): bool
+ {
+ return $this->pdo()->commit();
+ }
+
+ public function rollBack(): bool
+ {
+ return $this->pdo()->rollBack();
+ }
+
+ public function errorCode(): mixed
+ {
+ return $this->pdo()->errorCode();
+ }
+
+ public function errorInfo(): array
+ {
+ return $this->pdo()->errorInfo();
+ }
+
+ public function exec($query): int
+ {
+ return $this->pdo()->exec($query);
+ }
+
+ public function prepare($statement, $options = array())
+ {
+ return $this->pdo()->prepare($statement, $options);
+ }
+
+ public function quote($string, $parameter_type = null): string
+ {
+ return $this->pdo()->quote($string, $parameter_type);
+ }
+
+ public function lastInsertId(/* ?string */$name = null): string
+ {
+ return $this->pdo()->lastInsertId($name);
+ }
+
+ public function query(string $statement): \PDOStatement
+ {
+ return call_user_func_array(array($this->pdo(), 'query'), func_get_args());
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Database/TypeConverter.php
+namespace Tqdev\PhpCrudApi\Database {
+
+ class TypeConverter
+ {
+ private $driver;
+
+ public function __construct(string $driver)
+ {
+ $this->driver = $driver;
+ }
+
+ private $fromJdbc = [
+ 'mysql' => [
+ 'clob' => 'longtext',
+ 'boolean' => 'tinyint(1)',
+ 'blob' => 'longblob',
+ 'timestamp' => 'datetime',
+ ],
+ 'pgsql' => [
+ 'clob' => 'text',
+ 'blob' => 'bytea',
+ 'float' => 'real',
+ 'double' => 'double precision',
+ 'varbinary' => 'bytea',
+ ],
+ 'sqlsrv' => [
+ 'boolean' => 'bit',
+ 'varchar' => 'nvarchar',
+ 'clob' => 'ntext',
+ 'blob' => 'image',
+ 'time' => 'time(0)',
+ 'timestamp' => 'datetime2(0)',
+ 'double' => 'float',
+ 'float' => 'real',
+ ],
+ ];
+
+ private $toJdbc = [
+ 'simplified' => [
+ 'char' => 'varchar',
+ 'longvarchar' => 'clob',
+ 'nchar' => 'varchar',
+ 'nvarchar' => 'varchar',
+ 'longnvarchar' => 'clob',
+ 'binary' => 'varbinary',
+ 'longvarbinary' => 'blob',
+ 'tinyint' => 'integer',
+ 'smallint' => 'integer',
+ 'real' => 'float',
+ 'numeric' => 'decimal',
+ 'nclob' => 'clob',
+ 'time_with_timezone' => 'time',
+ 'timestamp_with_timezone' => 'timestamp',
+ ],
+ 'mysql' => [
+ 'tinyint(1)' => 'boolean',
+ 'bit(1)' => 'boolean',
+ 'tinyblob' => 'blob',
+ 'mediumblob' => 'blob',
+ 'longblob' => 'blob',
+ 'tinytext' => 'clob',
+ 'mediumtext' => 'clob',
+ 'longtext' => 'clob',
+ 'text' => 'clob',
+ 'mediumint' => 'integer',
+ 'int' => 'integer',
+ 'polygon' => 'geometry',
+ 'point' => 'geometry',
+ 'datetime' => 'timestamp',
+ 'year' => 'integer',
+ 'enum' => 'varchar',
+ 'set' => 'varchar',
+ 'json' => 'clob',
+ ],
+ 'pgsql' => [
+ 'bigserial' => 'bigint',
+ 'bit varying' => 'bit',
+ 'box' => 'geometry',
+ 'bytea' => 'blob',
+ 'bpchar' => 'char',
+ 'character varying' => 'varchar',
+ 'character' => 'char',
+ 'cidr' => 'varchar',
+ 'circle' => 'geometry',
+ 'double precision' => 'double',
+ 'inet' => 'integer',
+ //'interval [ fields ]'
+ 'json' => 'clob',
+ 'jsonb' => 'clob',
+ 'line' => 'geometry',
+ 'lseg' => 'geometry',
+ 'macaddr' => 'varchar',
+ 'money' => 'decimal',
+ 'path' => 'geometry',
+ 'point' => 'geometry',
+ 'polygon' => 'geometry',
+ 'real' => 'float',
+ 'serial' => 'integer',
+ 'text' => 'clob',
+ 'time without time zone' => 'time',
+ 'time with time zone' => 'time_with_timezone',
+ 'timestamp without time zone' => 'timestamp',
+ 'timestamp with time zone' => 'timestamp_with_timezone',
+ //'tsquery'=
+ //'tsvector'
+ //'txid_snapshot'
+ 'uuid' => 'char',
+ 'xml' => 'clob',
+ ],
+ // source: https://docs.microsoft.com/en-us/sql/connect/jdbc/using-basic-data-types?view=sql-server-2017
+ 'sqlsrv' => [
+ 'varbinary()' => 'blob',
+ 'bit' => 'boolean',
+ 'datetime' => 'timestamp',
+ 'datetime2' => 'timestamp',
+ 'float' => 'double',
+ 'image' => 'blob',
+ 'int' => 'integer',
+ 'money' => 'decimal',
+ 'ntext' => 'clob',
+ 'smalldatetime' => 'timestamp',
+ 'smallmoney' => 'decimal',
+ 'text' => 'clob',
+ 'timestamp' => 'binary',
+ 'udt' => 'varbinary',
+ 'uniqueidentifier' => 'char',
+ 'xml' => 'clob',
+ ],
+ 'sqlite' => [
+ 'tinytext' => 'clob',
+ 'text' => 'clob',
+ 'mediumtext' => 'clob',
+ 'longtext' => 'clob',
+ 'mediumint' => 'integer',
+ 'int' => 'integer',
+ 'bigint' => 'bigint',
+ 'int2' => 'smallint',
+ 'int4' => 'integer',
+ 'int8' => 'bigint',
+ 'double precision' => 'double',
+ 'datetime' => 'timestamp'
+ ],
+ ];
+
+ // source: https://docs.oracle.com/javase/9/docs/api/java/sql/Types.html
+ private $valid = [
+ //'array' => true,
+ 'bigint' => true,
+ 'binary' => true,
+ 'bit' => true,
+ 'blob' => true,
+ 'boolean' => true,
+ 'char' => true,
+ 'clob' => true,
+ //'datalink' => true,
+ 'date' => true,
+ 'decimal' => true,
+ //'distinct' => true,
+ 'double' => true,
+ 'float' => true,
+ 'integer' => true,
+ //'java_object' => true,
+ 'longnvarchar' => true,
+ 'longvarbinary' => true,
+ 'longvarchar' => true,
+ 'nchar' => true,
+ 'nclob' => true,
+ //'null' => true,
+ 'numeric' => true,
+ 'nvarchar' => true,
+ //'other' => true,
+ 'real' => true,
+ //'ref' => true,
+ //'ref_cursor' => true,
+ //'rowid' => true,
+ 'smallint' => true,
+ //'sqlxml' => true,
+ //'struct' => true,
+ 'time' => true,
+ 'time_with_timezone' => true,
+ 'timestamp' => true,
+ 'timestamp_with_timezone' => true,
+ 'tinyint' => true,
+ 'varbinary' => true,
+ 'varchar' => true,
+ // extra:
+ 'geometry' => true,
+ ];
+
+ public function toJdbc(string $type, string $size): string
+ {
+ $jdbcType = strtolower($type);
+ if (isset($this->toJdbc[$this->driver]["$jdbcType($size)"])) {
+ $jdbcType = $this->toJdbc[$this->driver]["$jdbcType($size)"];
+ }
+ if (isset($this->toJdbc[$this->driver][$jdbcType])) {
+ $jdbcType = $this->toJdbc[$this->driver][$jdbcType];
+ }
+ if (isset($this->toJdbc['simplified'][$jdbcType])) {
+ $jdbcType = $this->toJdbc['simplified'][$jdbcType];
+ }
+ if (!isset($this->valid[$jdbcType])) {
+ //throw new \Exception("Unsupported type '$jdbcType' for driver '$this->driver'");
+ $jdbcType = 'clob';
+ }
+ return $jdbcType;
+ }
+
+ public function fromJdbc(string $type): string
+ {
+ $jdbcType = strtolower($type);
+ if (isset($this->fromJdbc[$this->driver][$jdbcType])) {
+ $jdbcType = $this->fromJdbc[$this->driver][$jdbcType];
+ }
+ return $jdbcType;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/GeoJson/Feature.php
+namespace Tqdev\PhpCrudApi\GeoJson {
+
+ class Feature implements \JsonSerializable
+ {
+ private $id;
+ private $properties;
+ private $geometry;
+
+ public function __construct($id, array $properties, /*?Geometry*/ $geometry)
+ {
+ $this->id = $id;
+ $this->properties = $properties;
+ $this->geometry = $geometry;
+ }
+
+ public function serialize()
+ {
+ return [
+ 'type' => 'Feature',
+ 'id' => $this->id,
+ 'properties' => $this->properties,
+ 'geometry' => $this->geometry,
+ ];
+ }
+
+ public function jsonSerialize()
+ {
+ return $this->serialize();
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/GeoJson/FeatureCollection.php
+namespace Tqdev\PhpCrudApi\GeoJson {
+
+ class FeatureCollection implements \JsonSerializable
+ {
+ private $features;
+
+ private $results;
+
+ public function __construct(array $features, int $results)
+ {
+ $this->features = $features;
+ $this->results = $results;
+ }
+
+ public function serialize()
+ {
+ return [
+ 'type' => 'FeatureCollection',
+ 'features' => $this->features,
+ 'results' => $this->results,
+ ];
+ }
+
+ public function jsonSerialize()
+ {
+ return array_filter($this->serialize(), function ($v) {
+ return $v !== 0;
+ });
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/GeoJson/GeoJsonService.php
+namespace Tqdev\PhpCrudApi\GeoJson {
+
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\GeoJson\FeatureCollection;
+ use Tqdev\PhpCrudApi\Record\RecordService;
+
+ class GeoJsonService
+ {
+ private $reflection;
+ private $records;
+
+ public function __construct(ReflectionService $reflection, RecordService $records)
+ {
+ $this->reflection = $reflection;
+ $this->records = $records;
+ }
+
+ public function hasTable(string $table): bool
+ {
+ return $this->reflection->hasTable($table);
+ }
+
+ public function getType(string $table): string
+ {
+ return $this->reflection->getType($table);
+ }
+
+ private function getGeometryColumnName(string $tableName, array &$params): string
+ {
+ $geometryParam = isset($params['geometry']) ? $params['geometry'][0] : '';
+ $table = $this->reflection->getTable($tableName);
+ $geometryColumnName = '';
+ foreach ($table->getColumnNames() as $columnName) {
+ if ($geometryParam && $geometryParam != $columnName) {
+ continue;
+ }
+ $column = $table->getColumn($columnName);
+ if ($column->isGeometry()) {
+ $geometryColumnName = $columnName;
+ break;
+ }
+ }
+ if ($geometryColumnName) {
+ $params['mandatory'][] = $tableName . "." . $geometryColumnName;
+ }
+ return $geometryColumnName;
+ }
+
+ private function setBoudingBoxFilter(string $geometryColumnName, array &$params)
+ {
+ $boundingBox = isset($params['bbox']) ? $params['bbox'][0] : '';
+ if ($boundingBox) {
+ $c = explode(',', $boundingBox);
+ if (!isset($params['filter'])) {
+ $params['filter'] = array();
+ }
+ $params['filter'][] = "$geometryColumnName,sin,POLYGON(($c[0] $c[1],$c[2] $c[1],$c[2] $c[3],$c[0] $c[3],$c[0] $c[1]))";
+ }
+ $tile = isset($params['tile']) ? $params['tile'][0] : '';
+ if ($tile) {
+ $zxy = explode(',', $tile);
+ if (count($zxy) == 3) {
+ list($z, $x, $y) = $zxy;
+ $c = array();
+ $c = array_merge($c, $this->convertTileToLatLonOfUpperLeftCorner($z, $x, $y));
+ $c = array_merge($c, $this->convertTileToLatLonOfUpperLeftCorner($z, $x + 1, $y + 1));
+ $params['filter'][] = "$geometryColumnName,sin,POLYGON(($c[0] $c[1],$c[2] $c[1],$c[2] $c[3],$c[0] $c[3],$c[0] $c[1]))";
+ }
+ }
+ }
+
+ private function convertTileToLatLonOfUpperLeftCorner($z, $x, $y): array
+ {
+ $n = pow(2, $z);
+ $lon = $x / $n * 360.0 - 180.0;
+ $lat = rad2deg(atan(sinh(pi() * (1 - 2 * $y / $n))));
+ return [$lon, $lat];
+ }
+
+ private function convertRecordToFeature(/*object*/$record, string $primaryKeyColumnName, string $geometryColumnName)
+ {
+ $id = null;
+ if ($primaryKeyColumnName) {
+ $id = $record[$primaryKeyColumnName];
+ }
+ $geometry = null;
+ if (isset($record[$geometryColumnName])) {
+ $geometry = Geometry::fromWkt($record[$geometryColumnName]);
+ }
+ $properties = array_diff_key($record, [$primaryKeyColumnName => true, $geometryColumnName => true]);
+ return new Feature($id, $properties, $geometry);
+ }
+
+ private function getPrimaryKeyColumnName(string $tableName, array &$params): string
+ {
+ $primaryKeyColumn = $this->reflection->getTable($tableName)->getPk();
+ if (!$primaryKeyColumn) {
+ return '';
+ }
+ $primaryKeyColumnName = $primaryKeyColumn->getName();
+ $params['mandatory'][] = $tableName . "." . $primaryKeyColumnName;
+ return $primaryKeyColumnName;
+ }
+
+ public function _list(string $tableName, array $params): FeatureCollection
+ {
+ $geometryColumnName = $this->getGeometryColumnName($tableName, $params);
+ $this->setBoudingBoxFilter($geometryColumnName, $params);
+ $primaryKeyColumnName = $this->getPrimaryKeyColumnName($tableName, $params);
+ $records = $this->records->_list($tableName, $params);
+ $features = array();
+ foreach ($records->getRecords() as $record) {
+ $features[] = $this->convertRecordToFeature($record, $primaryKeyColumnName, $geometryColumnName);
+ }
+ return new FeatureCollection($features, $records->getResults());
+ }
+
+ public function read(string $tableName, string $id, array $params): Feature
+ {
+ $geometryColumnName = $this->getGeometryColumnName($tableName, $params);
+ $primaryKeyColumnName = $this->getPrimaryKeyColumnName($tableName, $params);
+ $record = $this->records->read($tableName, $id, $params);
+ return $this->convertRecordToFeature($record, $primaryKeyColumnName, $geometryColumnName);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/GeoJson/Geometry.php
+namespace Tqdev\PhpCrudApi\GeoJson {
+
+ class Geometry implements \JsonSerializable
+ {
+ private $type;
+ private $geometry;
+
+ public static $types = [
+ "Point",
+ "MultiPoint",
+ "LineString",
+ "MultiLineString",
+ "Polygon",
+ "MultiPolygon",
+ //"GeometryCollection",
+ ];
+
+ public function __construct(string $type, array $coordinates)
+ {
+ $this->type = $type;
+ $this->coordinates = $coordinates;
+ }
+
+ public static function fromWkt(string $wkt): Geometry
+ {
+ $bracket = strpos($wkt, '(');
+ $type = strtoupper(trim(substr($wkt, 0, $bracket)));
+ $supported = false;
+ foreach (Geometry::$types as $typeName) {
+ if (strtoupper($typeName) == $type) {
+ $type = $typeName;
+ $supported = true;
+ }
+ }
+ if (!$supported) {
+ throw new \Exception('Geometry type not supported: ' . $type);
+ }
+ $coordinates = substr($wkt, $bracket);
+ if (substr($type, -5) != 'Point' || ($type == 'MultiPoint' && $coordinates[1] != '(')) {
+ $coordinates = preg_replace('|([0-9\-\.]+ )+([0-9\-\.]+)|', '[\1\2]', $coordinates);
+ }
+ $coordinates = str_replace(['(', ')', ', ', ' '], ['[', ']', ',', ','], $coordinates);
+ $coordinates = json_decode($coordinates);
+ if (!$coordinates) {
+ throw new \Exception('Could not decode WKT: ' . $wkt);
+ }
+ return new Geometry($type, $coordinates);
+ }
+
+ public function serialize()
+ {
+ return [
+ 'type' => $this->type,
+ 'coordinates' => $this->coordinates,
+ ];
+ }
+
+ public function jsonSerialize()
+ {
+ return $this->serialize();
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/Base/Middleware.php
+namespace Tqdev\PhpCrudApi\Middleware\Base {
+
+ use Psr\Http\Server\MiddlewareInterface;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+
+ abstract class Middleware implements MiddlewareInterface
+ {
+ protected $next;
+ protected $responder;
+ private $properties;
+
+ public function __construct(Router $router, Responder $responder, array $properties)
+ {
+ $router->load($this);
+ $this->responder = $responder;
+ $this->properties = $properties;
+ }
+
+ protected function getArrayProperty(string $key, string $default): array
+ {
+ return array_filter(array_map('trim', explode(',', $this->getProperty($key, $default))));
+ }
+
+ protected function getMapProperty(string $key, string $default): array
+ {
+ $pairs = $this->getArrayProperty($key, $default);
+ $result = array();
+ foreach ($pairs as $pair) {
+ if (strpos($pair, ':')) {
+ list($k, $v) = explode(':', $pair, 2);
+ $result[trim($k)] = trim($v);
+ } else {
+ $result[] = trim($pair);
+ }
+ }
+ return $result;
+ }
+
+ protected function getProperty(string $key, $default)
+ {
+ return isset($this->properties[$key]) ? $this->properties[$key] : $default;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/Communication/VariableStore.php
+namespace Tqdev\PhpCrudApi\Middleware\Communication {
+
+ class VariableStore
+ {
+ public static $values = array();
+
+ public static function get(string $key)
+ {
+ if (isset(self::$values[$key])) {
+ return self::$values[$key];
+ }
+ return null;
+ }
+
+ public static function set(string $key, /* object */ $value)
+ {
+ self::$values[$key] = $value;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/Router/Router.php
+namespace Tqdev\PhpCrudApi\Middleware\Router {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+
+ interface Router extends RequestHandlerInterface
+ {
+ public function register(string $method, string $path, array $handler);
+
+ public function load(Middleware $middleware);
+
+ public function route(ServerRequestInterface $request): ResponseInterface;
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/Router/SimpleRouter.php
+namespace Tqdev\PhpCrudApi\Middleware\Router {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Tqdev\PhpCrudApi\Cache\Cache;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\Record\PathTree;
+ use Tqdev\PhpCrudApi\RequestUtils;
+ use Tqdev\PhpCrudApi\ResponseUtils;
+
+ class SimpleRouter implements Router
+ {
+ private $basePath;
+ private $responder;
+ private $cache;
+ private $ttl;
+ private $debug;
+ private $registration;
+ private $routes;
+ private $routeHandlers;
+ private $middlewares;
+
+ public function __construct(string $basePath, Responder $responder, Cache $cache, int $ttl, bool $debug)
+ {
+ $this->basePath = rtrim($this->detectBasePath($basePath), '/');
+ $this->responder = $responder;
+ $this->cache = $cache;
+ $this->ttl = $ttl;
+ $this->debug = $debug;
+ $this->registration = true;
+ $this->routes = $this->loadPathTree();
+ $this->routeHandlers = [];
+ $this->middlewares = array();
+ }
+
+ private function detectBasePath(string $basePath): string
+ {
+ if ($basePath) {
+ return $basePath;
+ }
+ if (isset($_SERVER['REQUEST_URI'])) {
+ $fullPath = urldecode(explode('?', $_SERVER['REQUEST_URI'])[0]);
+ if (isset($_SERVER['PATH_INFO'])) {
+ $path = $_SERVER['PATH_INFO'];
+ if (substr($fullPath, -1 * strlen($path)) == $path) {
+ return substr($fullPath, 0, -1 * strlen($path));
+ }
+ }
+ if ('/' . basename(__FILE__) == $fullPath) {
+ return $fullPath;
+ }
+ }
+ return '/';
+ }
+
+ private function loadPathTree(): PathTree
+ {
+ $data = $this->cache->get('PathTree');
+ if ($data != '') {
+ $tree = PathTree::fromJson(json_decode(gzuncompress($data)));
+ $this->registration = false;
+ } else {
+ $tree = new PathTree();
+ }
+ return $tree;
+ }
+
+ public function register(string $method, string $path, array $handler)
+ {
+ $routeNumber = count($this->routeHandlers);
+ $this->routeHandlers[$routeNumber] = $handler;
+ if ($this->registration) {
+ $path = trim($path, '/');
+ $parts = array();
+ if ($path) {
+ $parts = explode('/', $path);
+ }
+ array_unshift($parts, $method);
+ $this->routes->put($parts, $routeNumber);
+ }
+ }
+
+ public function load(Middleware $middleware) /*: void*/
+ {
+ array_push($this->middlewares, $middleware);
+ }
+
+ public function route(ServerRequestInterface $request): ResponseInterface
+ {
+ if ($this->registration) {
+ $data = gzcompress(json_encode($this->routes, JSON_UNESCAPED_UNICODE));
+ $this->cache->set('PathTree', $data, $this->ttl);
+ }
+
+ return $this->handle($request);
+ }
+
+ private function getRouteNumbers(ServerRequestInterface $request): array
+ {
+ $method = strtoupper($request->getMethod());
+ $path = array();
+ $segment = $method;
+ for ($i = 1; strlen($segment) > 0; $i++) {
+ array_push($path, $segment);
+ $segment = RequestUtils::getPathSegment($request, $i);
+ }
+ return $this->routes->match($path);
+ }
+
+ private function removeBasePath(ServerRequestInterface $request): ServerRequestInterface
+ {
+ $path = $request->getUri()->getPath();
+ if (substr($path, 0, strlen($this->basePath)) == $this->basePath) {
+ $path = substr($path, strlen($this->basePath));
+ $request = $request->withUri($request->getUri()->withPath($path));
+ }
+ return $request;
+ }
+
+ public function getBasePath(): string
+ {
+ return $this->basePath;
+ }
+
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $request = $this->removeBasePath($request);
+
+ if (count($this->middlewares)) {
+ $handler = array_pop($this->middlewares);
+ return $handler->process($request, $this);
+ }
+
+ $routeNumbers = $this->getRouteNumbers($request);
+ if (count($routeNumbers) == 0) {
+ return $this->responder->error(ErrorCode::ROUTE_NOT_FOUND, $request->getUri()->getPath());
+ }
+ try {
+ $response = call_user_func($this->routeHandlers[$routeNumbers[0]], $request);
+ } catch (\PDOException $e) {
+ if (strpos(strtolower($e->getMessage()), 'duplicate') !== false) {
+ $response = $this->responder->error(ErrorCode::DUPLICATE_KEY_EXCEPTION, '');
+ } elseif (strpos(strtolower($e->getMessage()), 'unique constraint') !== false) {
+ $response = $this->responder->error(ErrorCode::DUPLICATE_KEY_EXCEPTION, '');
+ } elseif (strpos(strtolower($e->getMessage()), 'default value') !== false) {
+ $response = $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
+ } elseif (strpos(strtolower($e->getMessage()), 'allow nulls') !== false) {
+ $response = $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
+ } elseif (strpos(strtolower($e->getMessage()), 'constraint') !== false) {
+ $response = $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
+ } else {
+ $response = $this->responder->error(ErrorCode::ERROR_NOT_FOUND, '');
+ }
+ if ($this->debug) {
+ $response = ResponseUtils::addExceptionHeaders($response, $e);
+ }
+ }
+ return $response;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/AjaxOnlyMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class AjaxOnlyMiddleware extends Middleware
+ {
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $method = $request->getMethod();
+ $excludeMethods = $this->getArrayProperty('excludeMethods', 'OPTIONS,GET');
+ if (!in_array($method, $excludeMethods)) {
+ $headerName = $this->getProperty('headerName', 'X-Requested-With');
+ $headerValue = $this->getProperty('headerValue', 'XMLHttpRequest');
+ if ($headerValue != RequestUtils::getHeader($request, $headerName)) {
+ return $this->responder->error(ErrorCode::ONLY_AJAX_REQUESTS_ALLOWED, $method);
+ }
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/AuthorizationMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Communication\VariableStore;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\Record\FilterInfo;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class AuthorizationMiddleware extends Middleware
+ {
+ private $reflection;
+
+ public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->reflection = $reflection;
+ }
+
+ private function handleColumns(string $operation, string $tableName) /*: void*/
+ {
+ $columnHandler = $this->getProperty('columnHandler', '');
+ if ($columnHandler) {
+ $table = $this->reflection->getTable($tableName);
+ foreach ($table->getColumnNames() as $columnName) {
+ $allowed = call_user_func($columnHandler, $operation, $tableName, $columnName);
+ if (!$allowed) {
+ $table->removeColumn($columnName);
+ }
+ }
+ }
+ }
+
+ private function handleTable(string $operation, string $tableName) /*: void*/
+ {
+ if (!$this->reflection->hasTable($tableName)) {
+ return;
+ }
+ $allowed = true;
+ $tableHandler = $this->getProperty('tableHandler', '');
+ if ($tableHandler) {
+ $allowed = call_user_func($tableHandler, $operation, $tableName);
+ }
+ if (!$allowed) {
+ $this->reflection->removeTable($tableName);
+ } else {
+ $this->handleColumns($operation, $tableName);
+ }
+ }
+
+ private function handleRecords(string $operation, string $tableName) /*: void*/
+ {
+ if (!$this->reflection->hasTable($tableName)) {
+ return;
+ }
+ $recordHandler = $this->getProperty('recordHandler', '');
+ if ($recordHandler) {
+ $query = call_user_func($recordHandler, $operation, $tableName);
+ $filters = new FilterInfo();
+ $table = $this->reflection->getTable($tableName);
+ $query = str_replace('][]=', ']=', str_replace('=', '[]=', $query));
+ parse_str($query, $params);
+ $condition = $filters->getCombinedConditions($table, $params);
+ VariableStore::set("authorization.conditions.$tableName", $condition);
+ }
+ }
+
+ private function pathHandler(string $path) /*: bool*/
+ {
+ $pathHandler = $this->getProperty('pathHandler', '');
+ return $pathHandler ? call_user_func($pathHandler, $path) : true;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $path = RequestUtils::getPathSegment($request, 1);
+
+ if (!$this->pathHandler($path)) {
+ return $this->responder->error(ErrorCode::ROUTE_NOT_FOUND, $request->getUri()->getPath());
+ }
+
+ $operation = RequestUtils::getOperation($request);
+ $tableNames = RequestUtils::getTableNames($request, $this->reflection);
+ foreach ($tableNames as $tableName) {
+ $this->handleTable($operation, $tableName);
+ if ($path == 'records') {
+ $this->handleRecords($operation, $tableName);
+ }
+ }
+ if ($path == 'openapi') {
+ VariableStore::set('authorization.tableHandler', $this->getProperty('tableHandler', ''));
+ VariableStore::set('authorization.columnHandler', $this->getProperty('columnHandler', ''));
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/BasicAuthMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class BasicAuthMiddleware extends Middleware
+ {
+ private function hasCorrectPassword(string $username, string $password, array &$passwords): bool
+ {
+ $hash = isset($passwords[$username]) ? $passwords[$username] : false;
+ if ($hash && password_verify($password, $hash)) {
+ if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
+ $passwords[$username] = password_hash($password, PASSWORD_DEFAULT);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private function getValidUsername(string $username, string $password, string $passwordFile): string
+ {
+ $passwords = $this->readPasswords($passwordFile);
+ $valid = $this->hasCorrectPassword($username, $password, $passwords);
+ $this->writePasswords($passwordFile, $passwords);
+ return $valid ? $username : '';
+ }
+
+ private function readPasswords(string $passwordFile): array
+ {
+ $passwords = [];
+ $passwordLines = file($passwordFile);
+ foreach ($passwordLines as $passwordLine) {
+ if (strpos($passwordLine, ':') !== false) {
+ list($username, $hash) = explode(':', trim($passwordLine), 2);
+ if (strlen($hash) > 0 && $hash[0] != '$') {
+ $hash = password_hash($hash, PASSWORD_DEFAULT);
+ }
+ $passwords[$username] = $hash;
+ }
+ }
+ return $passwords;
+ }
+
+ private function writePasswords(string $passwordFile, array $passwords): bool
+ {
+ $success = false;
+ $passwordFileContents = '';
+ foreach ($passwords as $username => $hash) {
+ $passwordFileContents .= "$username:$hash\n";
+ }
+ if (file_get_contents($passwordFile) != $passwordFileContents) {
+ $success = file_put_contents($passwordFile, $passwordFileContents) !== false;
+ }
+ return $success;
+ }
+
+ private function getAuthorizationCredentials(ServerRequestInterface $request): string
+ {
+ if (isset($_SERVER['PHP_AUTH_USER'])) {
+ return $_SERVER['PHP_AUTH_USER'] . ':' . $_SERVER['PHP_AUTH_PW'];
+ }
+ $header = RequestUtils::getHeader($request, 'Authorization');
+ $parts = explode(' ', trim($header), 2);
+ if (count($parts) != 2) {
+ return '';
+ }
+ if ($parts[0] != 'Basic') {
+ return '';
+ }
+ return base64_decode(strtr($parts[1], '-_', '+/'));
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ if (session_status() == PHP_SESSION_NONE) {
+ if (!headers_sent()) {
+ $sessionName = $this->getProperty('sessionName', '');
+ if ($sessionName) {
+ session_name($sessionName);
+ }
+ session_start();
+ }
+ }
+ $credentials = $this->getAuthorizationCredentials($request);
+ if ($credentials) {
+ list($username, $password) = array('', '');
+ if (strpos($credentials, ':') !== false) {
+ list($username, $password) = explode(':', $credentials, 2);
+ }
+ $passwordFile = $this->getProperty('passwordFile', '.htpasswd');
+ $validUser = $this->getValidUsername($username, $password, $passwordFile);
+ $_SESSION['username'] = $validUser;
+ if (!$validUser) {
+ return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username);
+ }
+ if (!headers_sent()) {
+ session_regenerate_id();
+ }
+ }
+ if (!isset($_SESSION['username']) || !$_SESSION['username']) {
+ $authenticationMode = $this->getProperty('mode', 'required');
+ if ($authenticationMode == 'required') {
+ $response = $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, '');
+ $realm = $this->getProperty('realm', 'Username and password required');
+ $response = $response->withHeader('WWW-Authenticate', "Basic realm=\"$realm\"");
+ return $response;
+ }
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/CorsMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\ResponseFactory;
+ use Tqdev\PhpCrudApi\ResponseUtils;
+
+ class CorsMiddleware extends Middleware
+ {
+ private $debug;
+
+ public function __construct(Router $router, Responder $responder, array $properties, bool $debug)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->debug = $debug;
+ }
+
+ private function isOriginAllowed(string $origin, string $allowedOrigins): bool
+ {
+ $found = false;
+ foreach (explode(',', $allowedOrigins) as $allowedOrigin) {
+ $hostname = preg_quote(strtolower(trim($allowedOrigin)));
+ $regex = '/^' . str_replace('\*', '.*', $hostname) . '$/';
+ if (preg_match($regex, $origin)) {
+ $found = true;
+ break;
+ }
+ }
+ return $found;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $method = $request->getMethod();
+ $origin = count($request->getHeader('Origin')) ? $request->getHeader('Origin')[0] : '';
+ $allowedOrigins = $this->getProperty('allowedOrigins', '*');
+ if ($origin && !$this->isOriginAllowed($origin, $allowedOrigins)) {
+ $response = $this->responder->error(ErrorCode::ORIGIN_FORBIDDEN, $origin);
+ } elseif ($method == 'OPTIONS') {
+ $response = ResponseFactory::fromStatus(ResponseFactory::OK);
+ $allowHeaders = $this->getProperty('allowHeaders', 'Content-Type, X-XSRF-TOKEN, X-Authorization');
+ if ($this->debug) {
+ $allowHeaders = implode(', ', array_filter([$allowHeaders, 'X-Exception-Name, X-Exception-Message, X-Exception-File']));
+ }
+ if ($allowHeaders) {
+ $response = $response->withHeader('Access-Control-Allow-Headers', $allowHeaders);
+ }
+ $allowMethods = $this->getProperty('allowMethods', 'OPTIONS, GET, PUT, POST, DELETE, PATCH');
+ if ($allowMethods) {
+ $response = $response->withHeader('Access-Control-Allow-Methods', $allowMethods);
+ }
+ $allowCredentials = $this->getProperty('allowCredentials', 'true');
+ if ($allowCredentials) {
+ $response = $response->withHeader('Access-Control-Allow-Credentials', $allowCredentials);
+ }
+ $maxAge = $this->getProperty('maxAge', '1728000');
+ if ($maxAge) {
+ $response = $response->withHeader('Access-Control-Max-Age', $maxAge);
+ }
+ $exposeHeaders = $this->getProperty('exposeHeaders', '');
+ if ($this->debug) {
+ $exposeHeaders = implode(', ', array_filter([$exposeHeaders, 'X-Exception-Name, X-Exception-Message, X-Exception-File']));
+ }
+ if ($exposeHeaders) {
+ $response = $response->withHeader('Access-Control-Expose-Headers', $exposeHeaders);
+ }
+ } else {
+ $response = null;
+ try {
+ $response = $next->handle($request);
+ } catch (\Throwable $e) {
+ $response = $this->responder->error(ErrorCode::ERROR_NOT_FOUND, $e->getMessage());
+ if ($this->debug) {
+ $response = ResponseUtils::addExceptionHeaders($response, $e);
+ }
+ }
+ }
+ if ($origin) {
+ $allowCredentials = $this->getProperty('allowCredentials', 'true');
+ if ($allowCredentials) {
+ $response = $response->withHeader('Access-Control-Allow-Credentials', $allowCredentials);
+ }
+ $response = $response->withHeader('Access-Control-Allow-Origin', $origin);
+ }
+ return $response;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/CustomizationMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class CustomizationMiddleware extends Middleware
+ {
+ private $reflection;
+
+ public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->reflection = $reflection;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $operation = RequestUtils::getOperation($request);
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ $beforeHandler = $this->getProperty('beforeHandler', '');
+ $environment = (object) array();
+ if ($beforeHandler !== '') {
+ $result = call_user_func($beforeHandler, $operation, $tableName, $request, $environment);
+ $request = $result ?: $request;
+ }
+ $response = $next->handle($request);
+ $afterHandler = $this->getProperty('afterHandler', '');
+ if ($afterHandler !== '') {
+ $result = call_user_func($afterHandler, $operation, $tableName, $response, $environment);
+ $response = $result ?: $response;
+ }
+ return $response;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Database\GenericDB;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\Record\Condition\ColumnCondition;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\Record\OrderingInfo;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class DbAuthMiddleware extends Middleware
+ {
+ private $reflection;
+ private $db;
+ private $ordering;
+
+ public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection, GenericDB $db)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->reflection = $reflection;
+ $this->db = $db;
+ $this->ordering = new OrderingInfo();
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ if (session_status() == PHP_SESSION_NONE) {
+ if (!headers_sent()) {
+ $sessionName = $this->getProperty('sessionName', '');
+ if ($sessionName) {
+ session_name($sessionName);
+ }
+ session_start();
+ }
+ }
+ $path = RequestUtils::getPathSegment($request, 1);
+ $method = $request->getMethod();
+ if ($method == 'POST' && in_array($path, ['login', 'register', 'password'])) {
+ $body = $request->getParsedBody();
+ $username = isset($body->username) ? $body->username : '';
+ $password = isset($body->password) ? $body->password : '';
+ $newPassword = isset($body->newPassword) ? $body->newPassword : '';
+ $tableName = $this->getProperty('usersTable', 'users');
+ $table = $this->reflection->getTable($tableName);
+ $usernameColumnName = $this->getProperty('usernameColumn', 'username');
+ $usernameColumn = $table->getColumn($usernameColumnName);
+ $passwordColumnName = $this->getProperty('passwordColumn', 'password');
+ $passwordLength = $this->getProperty('passwordLength', '12');
+ $pkName = $table->getPk()->getName();
+ $registerUser = $this->getProperty('registerUser', '');
+ $condition = new ColumnCondition($usernameColumn, 'eq', $username);
+ $returnedColumns = $this->getProperty('returnedColumns', '');
+ if (!$returnedColumns) {
+ $columnNames = $table->getColumnNames();
+ } else {
+ $columnNames = array_map('trim', explode(',', $returnedColumns));
+ $columnNames[] = $passwordColumnName;
+ $columnNames[] = $pkName;
+ }
+ $columnOrdering = $this->ordering->getDefaultColumnOrdering($table);
+ if ($path == 'register') {
+ if (!$registerUser) {
+ return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username);
+ }
+ if (strlen($password) < $passwordLength) {
+ return $this->responder->error(ErrorCode::PASSWORD_TOO_SHORT, $passwordLength);
+ }
+ $users = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, 0, 1);
+ if (!empty($users)) {
+ return $this->responder->error(ErrorCode::USER_ALREADY_EXIST, $username);
+ }
+ $data = json_decode($registerUser, true);
+ $data = is_array($data) ? $data : [];
+ $data[$usernameColumnName] = $username;
+ $data[$passwordColumnName] = password_hash($password, PASSWORD_DEFAULT);
+ $this->db->createSingle($table, $data);
+ $users = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, 0, 1);
+ foreach ($users as $user) {
+ unset($user[$passwordColumnName]);
+ return $this->responder->success($user);
+ }
+ return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username);
+ }
+ if ($path == 'login') {
+ $users = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, 0, 1);
+ foreach ($users as $user) {
+ if (password_verify($password, $user[$passwordColumnName]) == 1) {
+ if (!headers_sent()) {
+ session_regenerate_id(true);
+ }
+ unset($user[$passwordColumnName]);
+ $_SESSION['user'] = $user;
+ return $this->responder->success($user);
+ }
+ }
+ return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username);
+ }
+ if ($path == 'password') {
+ if ($username != ($_SESSION['user'][$usernameColumnName] ?? '')) {
+ return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username);
+ }
+ if (strlen($newPassword) < $passwordLength) {
+ return $this->responder->error(ErrorCode::PASSWORD_TOO_SHORT, $passwordLength);
+ }
+ $users = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, 0, 1);
+ foreach ($users as $user) {
+ if (password_verify($password, $user[$passwordColumnName]) == 1) {
+ if (!headers_sent()) {
+ session_regenerate_id(true);
+ }
+ $data = [$passwordColumnName => password_hash($newPassword, PASSWORD_DEFAULT)];
+ $this->db->updateSingle($table, $data, $user[$pkName]);
+ unset($user[$passwordColumnName]);
+ return $this->responder->success($user);
+ }
+ }
+ return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username);
+ }
+ }
+ if ($method == 'POST' && $path == 'logout') {
+ if (isset($_SESSION['user'])) {
+ $user = $_SESSION['user'];
+ unset($_SESSION['user']);
+ if (session_status() != PHP_SESSION_NONE) {
+ session_destroy();
+ }
+ return $this->responder->success($user);
+ }
+ return $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, '');
+ }
+ if ($method == 'GET' && $path == 'me') {
+ if (isset($_SESSION['user'])) {
+ return $this->responder->success($_SESSION['user']);
+ }
+ return $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, '');
+ }
+ if (!isset($_SESSION['user']) || !$_SESSION['user']) {
+ $authenticationMode = $this->getProperty('mode', 'required');
+ if ($authenticationMode == 'required') {
+ return $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, '');
+ }
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/FirewallMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+
+ class FirewallMiddleware extends Middleware
+ {
+ private function ipMatch(string $ip, string $cidr): bool
+ {
+ if (strpos($cidr, '/') !== false) {
+ list($subnet, $mask) = explode('/', trim($cidr));
+ if ((ip2long($ip) & ~((1 << (32 - $mask)) - 1)) == ip2long($subnet)) {
+ return true;
+ }
+ } else {
+ if (ip2long($ip) == ip2long($cidr)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private function isIpAllowed(string $ipAddress, string $allowedIpAddresses): bool
+ {
+ foreach (explode(',', $allowedIpAddresses) as $allowedIp) {
+ if ($this->ipMatch($ipAddress, $allowedIp)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $reverseProxy = $this->getProperty('reverseProxy', '');
+ if ($reverseProxy) {
+ $ipAddress = array_pop(explode(',', $request->getHeader('X-Forwarded-For')));
+ } elseif (isset($_SERVER['REMOTE_ADDR'])) {
+ $ipAddress = $_SERVER['REMOTE_ADDR'];
+ } else {
+ $ipAddress = '127.0.0.1';
+ }
+ $allowedIpAddresses = $this->getProperty('allowedIpAddresses', '');
+ if (!$this->isIpAllowed($ipAddress, $allowedIpAddresses)) {
+ $response = $this->responder->error(ErrorCode::TEMPORARY_OR_PERMANENTLY_BLOCKED, '');
+ } else {
+ $response = $next->handle($request);
+ }
+ return $response;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/IpAddressMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class IpAddressMiddleware extends Middleware
+ {
+ private $reflection;
+
+ public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->reflection = $reflection;
+ }
+
+ private function callHandler($record, string $operation, ReflectedTable $table) /*: object */
+ {
+ $context = (array) $record;
+ $columnNames = $this->getProperty('columns', '');
+ if ($columnNames) {
+ foreach (explode(',', $columnNames) as $columnName) {
+ if ($table->hasColumn($columnName)) {
+ if ($operation == 'create') {
+ $context[$columnName] = $_SERVER['REMOTE_ADDR'];
+ } else {
+ unset($context[$columnName]);
+ }
+ }
+ }
+ }
+ return (object) $context;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $operation = RequestUtils::getOperation($request);
+ if (in_array($operation, ['create', 'update', 'increment'])) {
+ $tableNames = $this->getProperty('tables', '');
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ if (!$tableNames || in_array($tableName, explode(',', $tableNames))) {
+ if ($this->reflection->hasTable($tableName)) {
+ $record = $request->getParsedBody();
+ if ($record !== null) {
+ $table = $this->reflection->getTable($tableName);
+ if (is_array($record)) {
+ foreach ($record as &$r) {
+ $r = $this->callHandler($r, $operation, $table);
+ }
+ } else {
+ $record = $this->callHandler($record, $operation, $table);
+ }
+ $request = $request->withParsedBody($record);
+ }
+ }
+ }
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/JoinLimitsMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Communication\VariableStore;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class JoinLimitsMiddleware extends Middleware
+ {
+ private $reflection;
+
+ public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->reflection = $reflection;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $operation = RequestUtils::getOperation($request);
+ $params = RequestUtils::getParams($request);
+ if (in_array($operation, ['read', 'list']) && isset($params['join'])) {
+ $maxDepth = (int) $this->getProperty('depth', '3');
+ $maxTables = (int) $this->getProperty('tables', '10');
+ $maxRecords = (int) $this->getProperty('records', '1000');
+ $tableCount = 0;
+ $joinPaths = array();
+ for ($i = 0; $i < count($params['join']); $i++) {
+ $joinPath = array();
+ $tables = explode(',', $params['join'][$i]);
+ for ($depth = 0; $depth < min($maxDepth, count($tables)); $depth++) {
+ array_push($joinPath, $tables[$depth]);
+ $tableCount += 1;
+ if ($tableCount == $maxTables) {
+ break;
+ }
+ }
+ array_push($joinPaths, implode(',', $joinPath));
+ if ($tableCount == $maxTables) {
+ break;
+ }
+ }
+ $params['join'] = $joinPaths;
+ $request = RequestUtils::setParams($request, $params);
+ VariableStore::set("joinLimits.maxRecords", $maxRecords);
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/JwtAuthMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class JwtAuthMiddleware extends Middleware
+ {
+ private function getVerifiedClaims(string $token, int $time, int $leeway, int $ttl, array $secrets, array $requirements): array
+ {
+ $algorithms = array(
+ 'HS256' => 'sha256',
+ 'HS384' => 'sha384',
+ 'HS512' => 'sha512',
+ 'RS256' => 'sha256',
+ 'RS384' => 'sha384',
+ 'RS512' => 'sha512',
+ );
+ $token = explode('.', $token);
+ if (count($token) < 3) {
+ return array();
+ }
+ $header = json_decode(base64_decode(strtr($token[0], '-_', '+/')), true);
+ $kid = 0;
+ if (isset($header['kid'])) {
+ $kid = $header['kid'];
+ }
+ if (!isset($secrets[$kid])) {
+ return array();
+ }
+ $secret = $secrets[$kid];
+ if ($header['typ'] != 'JWT') {
+ return array();
+ }
+ $algorithm = $header['alg'];
+ if (!isset($algorithms[$algorithm])) {
+ return array();
+ }
+ if (!empty($requirements['alg']) && !in_array($algorithm, $requirements['alg'])) {
+ return array();
+ }
+ $hmac = $algorithms[$algorithm];
+ $signature = base64_decode(strtr($token[2], '-_', '+/'));
+ $data = "$token[0].$token[1]";
+ switch ($algorithm[0]) {
+ case 'H':
+ $hash = hash_hmac($hmac, $data, $secret, true);
+ $equals = hash_equals($hash, $signature);
+ if (!$equals) {
+ return array();
+ }
+ break;
+ case 'R':
+ $equals = openssl_verify($data, $signature, $secret, $hmac) == 1;
+ if (!$equals) {
+ return array();
+ }
+ break;
+ }
+ $claims = json_decode(base64_decode(strtr($token[1], '-_', '+/')), true);
+ if (!$claims) {
+ return array();
+ }
+ foreach ($requirements as $field => $values) {
+ if (!empty($values)) {
+ if ($field != 'alg') {
+ if (!isset($claims[$field]) || !in_array($claims[$field], $values)) {
+ return array();
+ }
+ }
+ }
+ }
+ if (isset($claims['nbf']) && $time + $leeway < $claims['nbf']) {
+ return array();
+ }
+ if (isset($claims['iat']) && $time + $leeway < $claims['iat']) {
+ return array();
+ }
+ if (isset($claims['exp']) && $time - $leeway > $claims['exp']) {
+ return array();
+ }
+ if (isset($claims['iat']) && !isset($claims['exp'])) {
+ if ($time - $leeway > $claims['iat'] + $ttl) {
+ return array();
+ }
+ }
+ return $claims;
+ }
+
+ private function getClaims(string $token): array
+ {
+ $time = (int) $this->getProperty('time', time());
+ $leeway = (int) $this->getProperty('leeway', '5');
+ $ttl = (int) $this->getProperty('ttl', '30');
+ $secrets = $this->getMapProperty('secrets', '');
+ if (!$secrets) {
+ $secrets = [$this->getProperty('secret', '')];
+ }
+ $requirements = array(
+ 'alg' => $this->getArrayProperty('algorithms', ''),
+ 'aud' => $this->getArrayProperty('audiences', ''),
+ 'iss' => $this->getArrayProperty('issuers', ''),
+ );
+ return $this->getVerifiedClaims($token, $time, $leeway, $ttl, $secrets, $requirements);
+ }
+
+ private function getAuthorizationToken(ServerRequestInterface $request): string
+ {
+ $headerName = $this->getProperty('header', 'X-Authorization');
+ $headerValue = RequestUtils::getHeader($request, $headerName);
+ $parts = explode(' ', trim($headerValue), 2);
+ if (count($parts) != 2) {
+ return '';
+ }
+ if ($parts[0] != 'Bearer') {
+ return '';
+ }
+ return $parts[1];
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ if (session_status() == PHP_SESSION_NONE) {
+ if (!headers_sent()) {
+ $sessionName = $this->getProperty('sessionName', '');
+ if ($sessionName) {
+ session_name($sessionName);
+ }
+ session_start();
+ }
+ }
+ $token = $this->getAuthorizationToken($request);
+ if ($token) {
+ $claims = $this->getClaims($token);
+ $_SESSION['claims'] = $claims;
+ if (empty($claims)) {
+ return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, 'JWT');
+ }
+ if (!headers_sent()) {
+ session_regenerate_id();
+ }
+ }
+ if (empty($_SESSION['claims'])) {
+ $authenticationMode = $this->getProperty('mode', 'required');
+ if ($authenticationMode == 'required') {
+ return $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, '');
+ }
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/MultiTenancyMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Communication\VariableStore;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\Record\Condition\ColumnCondition;
+ use Tqdev\PhpCrudApi\Record\Condition\Condition;
+ use Tqdev\PhpCrudApi\Record\Condition\NoCondition;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class MultiTenancyMiddleware extends Middleware
+ {
+ private $reflection;
+
+ public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->reflection = $reflection;
+ }
+
+ private function getCondition(string $tableName, array $pairs): Condition
+ {
+ $condition = new NoCondition();
+ $table = $this->reflection->getTable($tableName);
+ foreach ($pairs as $k => $v) {
+ $condition = $condition->_and(new ColumnCondition($table->getColumn($k), 'eq', $v));
+ }
+ return $condition;
+ }
+
+ private function getPairs($handler, string $operation, string $tableName): array
+ {
+ $result = array();
+ $pairs = call_user_func($handler, $operation, $tableName) ?: [];
+ $table = $this->reflection->getTable($tableName);
+ foreach ($pairs as $k => $v) {
+ if ($table->hasColumn($k)) {
+ $result[$k] = $v;
+ }
+ }
+ return $result;
+ }
+
+ private function handleRecord(ServerRequestInterface $request, string $operation, array $pairs): ServerRequestInterface
+ {
+ $record = $request->getParsedBody();
+ if ($record === null) {
+ return $request;
+ }
+ $multi = is_array($record);
+ $records = $multi ? $record : [$record];
+ foreach ($records as &$record) {
+ foreach ($pairs as $column => $value) {
+ if ($operation == 'create') {
+ $record->$column = $value;
+ } else {
+ if (isset($record->$column)) {
+ unset($record->$column);
+ }
+ }
+ }
+ }
+ return $request->withParsedBody($multi ? $records : $records[0]);
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $handler = $this->getProperty('handler', '');
+ if ($handler !== '') {
+ $path = RequestUtils::getPathSegment($request, 1);
+ if ($path == 'records') {
+ $operation = RequestUtils::getOperation($request);
+ $tableNames = RequestUtils::getTableNames($request, $this->reflection);
+ foreach ($tableNames as $i => $tableName) {
+ if (!$this->reflection->hasTable($tableName)) {
+ continue;
+ }
+ $pairs = $this->getPairs($handler, $operation, $tableName);
+ if ($i == 0) {
+ if (in_array($operation, ['create', 'update', 'increment'])) {
+ $request = $this->handleRecord($request, $operation, $pairs);
+ }
+ }
+ $condition = $this->getCondition($tableName, $pairs);
+ VariableStore::set("multiTenancy.conditions.$tableName", $condition);
+ }
+ }
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/PageLimitsMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class PageLimitsMiddleware extends Middleware
+ {
+ private $reflection;
+
+ public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->reflection = $reflection;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $operation = RequestUtils::getOperation($request);
+ if ($operation == 'list') {
+ $params = RequestUtils::getParams($request);
+ $maxPage = (int) $this->getProperty('pages', '100');
+ if (isset($params['page']) && $params['page'] && $maxPage > 0) {
+ if (strpos($params['page'][0], ',') === false) {
+ $page = $params['page'][0];
+ } else {
+ list($page, $size) = explode(',', $params['page'][0], 2);
+ }
+ if ($page > $maxPage) {
+ return $this->responder->error(ErrorCode::PAGINATION_FORBIDDEN, '');
+ }
+ }
+ $maxSize = (int) $this->getProperty('records', '1000');
+ if (!isset($params['size']) || !$params['size'] && $maxSize > 0) {
+ $params['size'] = array($maxSize);
+ } else {
+ $params['size'] = array(min($params['size'][0], $maxSize));
+ }
+ $request = RequestUtils::setParams($request, $params);
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/ReconnectMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Database\GenericDB;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+
+ class ReconnectMiddleware extends Middleware
+ {
+ private $reflection;
+ private $db;
+
+ public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection, GenericDB $db)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->reflection = $reflection;
+ $this->db = $db;
+ }
+
+ private function getDriver(): string
+ {
+ $driverHandler = $this->getProperty('driverHandler', '');
+ if ($driverHandler) {
+ return call_user_func($driverHandler);
+ }
+ return '';
+ }
+
+ private function getAddress(): string
+ {
+ $addressHandler = $this->getProperty('addressHandler', '');
+ if ($addressHandler) {
+ return call_user_func($addressHandler);
+ }
+ return '';
+ }
+
+ private function getPort(): int
+ {
+ $portHandler = $this->getProperty('portHandler', '');
+ if ($portHandler) {
+ return call_user_func($portHandler);
+ }
+ return 0;
+ }
+
+ private function getDatabase(): string
+ {
+ $databaseHandler = $this->getProperty('databaseHandler', '');
+ if ($databaseHandler) {
+ return call_user_func($databaseHandler);
+ }
+ return '';
+ }
+
+ private function getTables(): array
+ {
+ $tablesHandler = $this->getProperty('tablesHandler', '');
+ if ($tablesHandler) {
+ return call_user_func($tablesHandler);
+ }
+ return [];
+ }
+
+ private function getUsername(): string
+ {
+ $usernameHandler = $this->getProperty('usernameHandler', '');
+ if ($usernameHandler) {
+ return call_user_func($usernameHandler);
+ }
+ return '';
+ }
+
+ private function getPassword(): string
+ {
+ $passwordHandler = $this->getProperty('passwordHandler', '');
+ if ($passwordHandler) {
+ return call_user_func($passwordHandler);
+ }
+ return '';
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $driver = $this->getDriver();
+ $address = $this->getAddress();
+ $port = $this->getPort();
+ $database = $this->getDatabase();
+ $tables = $this->getTables();
+ $username = $this->getUsername();
+ $password = $this->getPassword();
+ if ($driver || $address || $port || $database || $tables || $username || $password) {
+ $this->db->reconstruct($driver, $address, $port, $database, $tables, $username, $password);
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/SanitationMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class SanitationMiddleware extends Middleware
+ {
+ private $reflection;
+
+ public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->reflection = $reflection;
+ }
+
+ private function callHandler($handler, $record, string $operation, ReflectedTable $table) /*: object */
+ {
+ $context = (array) $record;
+ $tableName = $table->getName();
+ foreach ($context as $columnName => &$value) {
+ if ($table->hasColumn($columnName)) {
+ $column = $table->getColumn($columnName);
+ $value = call_user_func($handler, $operation, $tableName, $column->serialize(), $value);
+ $value = $this->sanitizeType($table, $column, $value);
+ }
+ }
+ return (object) $context;
+ }
+
+ private function sanitizeType(ReflectedTable $table, ReflectedColumn $column, $value)
+ {
+ $tables = $this->getArrayProperty('tables', 'all');
+ $types = $this->getArrayProperty('types', 'all');
+ if (
+ (in_array('all', $tables) || in_array($table->getName(), $tables)) &&
+ (in_array('all', $types) || in_array($column->getType(), $types))
+ ) {
+ if (is_null($value)) {
+ return $value;
+ }
+ if (is_string($value)) {
+ $newValue = null;
+ switch ($column->getType()) {
+ case 'integer':
+ case 'bigint':
+ $newValue = filter_var(trim($value), FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
+ break;
+ case 'decimal':
+ $newValue = filter_var(trim($value), FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE);
+ if (is_float($newValue)) {
+ $newValue = number_format($newValue, $column->getScale(), '.', '');
+ }
+ break;
+ case 'float':
+ case 'double':
+ $newValue = filter_var(trim($value), FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE);
+ break;
+ case 'boolean':
+ $newValue = filter_var(trim($value), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
+ break;
+ case 'date':
+ $time = strtotime(trim($value));
+ if ($time !== false) {
+ $newValue = date('Y-m-d', $time);
+ }
+ break;
+ case 'time':
+ $time = strtotime(trim($value));
+ if ($time !== false) {
+ $newValue = date('H:i:s', $time);
+ }
+ break;
+ case 'timestamp':
+ $time = strtotime(trim($value));
+ if ($time !== false) {
+ $newValue = date('Y-m-d H:i:s', $time);
+ }
+ break;
+ case 'blob':
+ case 'varbinary':
+ // allow base64url format
+ $newValue = strtr(trim($value), '-_', '+/');
+ break;
+ case 'clob':
+ case 'varchar':
+ $newValue = $value;
+ break;
+ case 'geometry':
+ $newValue = trim($value);
+ break;
+ }
+ if (!is_null($newValue)) {
+ $value = $newValue;
+ }
+ } else {
+ switch ($column->getType()) {
+ case 'integer':
+ case 'bigint':
+ if (is_float($value)) {
+ $value = (int) round($value);
+ }
+ break;
+ case 'decimal':
+ if (is_float($value) || is_int($value)) {
+ $value = number_format((float) $value, $column->getScale(), '.', '');
+ }
+ break;
+ }
+ }
+ // post process
+ }
+ return $value;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $operation = RequestUtils::getOperation($request);
+ if (in_array($operation, ['create', 'update', 'increment'])) {
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ if ($this->reflection->hasTable($tableName)) {
+ $record = $request->getParsedBody();
+ if ($record !== null) {
+ $handler = $this->getProperty('handler', '');
+ if ($handler !== '') {
+ $table = $this->reflection->getTable($tableName);
+ if (is_array($record)) {
+ foreach ($record as &$r) {
+ $r = $this->callHandler($handler, $r, $operation, $table);
+ }
+ } else {
+ $record = $this->callHandler($handler, $record, $operation, $table);
+ }
+ $request = $request->withParsedBody($record);
+ }
+ }
+ }
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/SslRedirectMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\ResponseFactory;
+
+ class SslRedirectMiddleware extends Middleware
+ {
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $uri = $request->getUri();
+ $scheme = $uri->getScheme();
+ if ($scheme == 'http') {
+ $uri = $request->getUri();
+ $uri = $uri->withScheme('https');
+ $response = ResponseFactory::fromStatus(301);
+ $response = $response->withHeader('Location', $uri->__toString());
+ } else {
+ $response = $next->handle($request);
+ }
+ return $response;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/ValidationMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class ValidationMiddleware extends Middleware
+ {
+ private $reflection;
+
+ public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->reflection = $reflection;
+ }
+
+ private function callHandler($handler, $record, string $operation, ReflectedTable $table) /*: ResponseInterface?*/
+ {
+ $context = (array) $record;
+ $details = array();
+ $tableName = $table->getName();
+ foreach ($context as $columnName => $value) {
+ if ($table->hasColumn($columnName)) {
+ $column = $table->getColumn($columnName);
+ $valid = call_user_func($handler, $operation, $tableName, $column->serialize(), $value, $context);
+ if ($valid === true || $valid === '') {
+ $valid = $this->validateType($table, $column, $value);
+ }
+ if ($valid !== true && $valid !== '') {
+ $details[$columnName] = $valid;
+ }
+ }
+ }
+ if (count($details) > 0) {
+ return $this->responder->error(ErrorCode::INPUT_VALIDATION_FAILED, $tableName, $details);
+ }
+ return null;
+ }
+
+ private function validateType(ReflectedTable $table, ReflectedColumn $column, $value)
+ {
+ $tables = $this->getArrayProperty('tables', 'all');
+ $types = $this->getArrayProperty('types', 'all');
+ if (
+ (in_array('all', $tables) || in_array($table->getName(), $tables)) &&
+ (in_array('all', $types) || in_array($column->getType(), $types))
+ ) {
+ if (is_null($value)) {
+ return ($column->getNullable() ? true : "cannot be null");
+ }
+ if (is_string($value)) {
+ // check for whitespace
+ switch ($column->getType()) {
+ case 'varchar':
+ case 'clob':
+ break;
+ default:
+ if (strlen(trim($value)) != strlen($value)) {
+ return 'illegal whitespace';
+ }
+ break;
+ }
+ // try to parse
+ switch ($column->getType()) {
+ case 'integer':
+ case 'bigint':
+ if (
+ filter_var($value, FILTER_SANITIZE_NUMBER_INT) !== $value ||
+ filter_var($value, FILTER_VALIDATE_INT) === false
+ ) {
+ return 'invalid integer';
+ }
+ break;
+ case 'decimal':
+ if (strpos($value, '.') !== false) {
+ list($whole, $decimals) = explode('.', ltrim($value, '-'), 2);
+ } else {
+ list($whole, $decimals) = array(ltrim($value, '-'), '');
+ }
+ if (strlen($whole) > 0 && !ctype_digit($whole)) {
+ return 'invalid decimal';
+ }
+ if (strlen($decimals) > 0 && !ctype_digit($decimals)) {
+ return 'invalid decimal';
+ }
+ if (strlen($whole) > $column->getPrecision() - $column->getScale()) {
+ return 'decimal too large';
+ }
+ if (strlen($decimals) > $column->getScale()) {
+ return 'decimal too precise';
+ }
+ break;
+ case 'float':
+ case 'double':
+ if (
+ filter_var($value, FILTER_SANITIZE_NUMBER_FLOAT) !== $value ||
+ filter_var($value, FILTER_VALIDATE_FLOAT) === false
+ ) {
+ return 'invalid float';
+ }
+ break;
+ case 'boolean':
+ if (!in_array(strtolower($value), array('true', 'false'))) {
+ return 'invalid boolean';
+ }
+ break;
+ case 'date':
+ if (date_create_from_format('Y-m-d', $value) === false) {
+ return 'invalid date';
+ }
+ break;
+ case 'time':
+ if (date_create_from_format('H:i:s', $value) === false) {
+ return 'invalid time';
+ }
+ break;
+ case 'timestamp':
+ if (date_create_from_format('Y-m-d H:i:s', $value) === false) {
+ return 'invalid timestamp';
+ }
+ break;
+ case 'clob':
+ case 'varchar':
+ if ($column->hasLength() && mb_strlen($value, 'UTF-8') > $column->getLength()) {
+ return 'string too long';
+ }
+ break;
+ case 'blob':
+ case 'varbinary':
+ if (base64_decode($value, true) === false) {
+ return 'invalid base64';
+ }
+ if ($column->hasLength() && strlen(base64_decode($value)) > $column->getLength()) {
+ return 'string too long';
+ }
+ break;
+ case 'geometry':
+ // no checks yet
+ break;
+ }
+ } else { // check non-string types
+ switch ($column->getType()) {
+ case 'integer':
+ case 'bigint':
+ if (!is_int($value)) {
+ return 'invalid integer';
+ }
+ break;
+ case 'float':
+ case 'double':
+ if (!is_float($value) && !is_int($value)) {
+ return 'invalid float';
+ }
+ break;
+ case 'boolean':
+ if (!is_bool($value) && ($value !== 0) && ($value !== 1)) {
+ return 'invalid boolean';
+ }
+ break;
+ default:
+ return 'invalid ' . $column->getType();
+ }
+ }
+ // extra checks
+ switch ($column->getType()) {
+ case 'integer': // 4 byte signed
+ $value = filter_var($value, FILTER_VALIDATE_INT);
+ if ($value > 2147483647 || $value < -2147483648) {
+ return 'invalid integer';
+ }
+ break;
+ }
+ }
+ return (true);
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $operation = RequestUtils::getOperation($request);
+ if (in_array($operation, ['create', 'update', 'increment'])) {
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ if ($this->reflection->hasTable($tableName)) {
+ $record = $request->getParsedBody();
+ if ($record !== null) {
+ $handler = $this->getProperty('handler', '');
+ if ($handler !== '') {
+ $table = $this->reflection->getTable($tableName);
+ if (is_array($record)) {
+ foreach ($record as $r) {
+ $response = $this->callHandler($handler, $r, $operation, $table);
+ if ($response !== null) {
+ return $response;
+ }
+ }
+ } else {
+ $response = $this->callHandler($handler, $record, $operation, $table);
+ if ($response !== null) {
+ return $response;
+ }
+ }
+ }
+ }
+ }
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/XmlMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\RequestUtils;
+ use Tqdev\PhpCrudApi\ResponseFactory;
+
+ class XmlMiddleware extends Middleware
+ {
+ private $reflection;
+
+ public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->reflection = $reflection;
+ }
+
+ private function json2xml($json, $types = 'null,boolean,number,string,object,array')
+ {
+ $a = json_decode($json);
+ $d = new \DOMDocument();
+ $c = $d->createElement("root");
+ $d->appendChild($c);
+ $t = function ($v) {
+ $type = gettype($v);
+ switch ($type) {
+ case 'integer':
+ return 'number';
+ case 'double':
+ return 'number';
+ default:
+ return strtolower($type);
+ }
+ };
+ $ts = explode(',', $types);
+ $f = function ($f, $c, $a, $s = false) use ($t, $d, $ts) {
+ if (in_array($t($a), $ts)) {
+ $c->setAttribute('type', $t($a));
+ }
+ if ($t($a) != 'array' && $t($a) != 'object') {
+ if ($t($a) == 'boolean') {
+ $c->appendChild($d->createTextNode($a ? 'true' : 'false'));
+ } else {
+ $c->appendChild($d->createTextNode($a));
+ }
+ } else {
+ foreach ($a as $k => $v) {
+ if ($k == '__type' && $t($a) == 'object') {
+ $c->setAttribute('__type', $v);
+ } else {
+ if ($t($v) == 'object') {
+ $ch = $c->appendChild($d->createElementNS(null, $s ? 'item' : $k));
+ $f($f, $ch, $v);
+ } else if ($t($v) == 'array') {
+ $ch = $c->appendChild($d->createElementNS(null, $s ? 'item' : $k));
+ $f($f, $ch, $v, true);
+ } else {
+ $va = $d->createElementNS(null, $s ? 'item' : $k);
+ if ($t($v) == 'boolean') {
+ $va->appendChild($d->createTextNode($v ? 'true' : 'false'));
+ } else {
+ $va->appendChild($d->createTextNode($v));
+ }
+ $ch = $c->appendChild($va);
+ if (in_array($t($v), $ts)) {
+ $ch->setAttribute('type', $t($v));
+ }
+ }
+ }
+ }
+ }
+ };
+ $f($f, $c, $a, $t($a) == 'array');
+ return $d->saveXML($d->documentElement);
+ }
+
+ private function xml2json($xml)
+ {
+ $a = @dom_import_simplexml(simplexml_load_string($xml));
+ if (!$a) {
+ return null;
+ }
+ $t = function ($v) {
+ $t = $v->getAttribute('type');
+ $txt = $v->firstChild->nodeType == XML_TEXT_NODE;
+ return $t ?: ($txt ? 'string' : 'object');
+ };
+ $f = function ($f, $a) use ($t) {
+ $c = null;
+ if ($t($a) == 'null') {
+ $c = null;
+ } else if ($t($a) == 'boolean') {
+ $b = substr(strtolower($a->textContent), 0, 1);
+ $c = in_array($b, array('1', 't'));
+ } else if ($t($a) == 'number') {
+ $c = $a->textContent + 0;
+ } else if ($t($a) == 'string') {
+ $c = $a->textContent;
+ } else if ($t($a) == 'object') {
+ $c = array();
+ if ($a->getAttribute('__type')) {
+ $c['__type'] = $a->getAttribute('__type');
+ }
+ for ($i = 0; $i < $a->childNodes->length; $i++) {
+ $v = $a->childNodes[$i];
+ $c[$v->nodeName] = $f($f, $v);
+ }
+ $c = (object) $c;
+ } else if ($t($a) == 'array') {
+ $c = array();
+ for ($i = 0; $i < $a->childNodes->length; $i++) {
+ $v = $a->childNodes[$i];
+ $c[$i] = $f($f, $v);
+ }
+ }
+ return $c;
+ };
+ $c = $f($f, $a);
+ return json_encode($c);
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ parse_str($request->getUri()->getQuery(), $params);
+ $isXml = isset($params['format']) && $params['format'] == 'xml';
+ if ($isXml) {
+ $body = $request->getBody()->getContents();
+ if ($body) {
+ $json = $this->xml2json($body);
+ $request = $request->withParsedBody(json_decode($json));
+ }
+ }
+ $response = $next->handle($request);
+ if ($isXml) {
+ $body = $response->getBody()->getContents();
+ if ($body) {
+ $types = implode(',', $this->getArrayProperty('types', 'null,array'));
+ if ($types == '' || $types == 'all') {
+ $xml = $this->json2xml($body);
+ } else {
+ $xml = $this->json2xml($body, $types);
+ }
+ $response = ResponseFactory::fromXml(ResponseFactory::OK, $xml);
+ }
+ }
+ return $response;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/XsrfMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+
+ class XsrfMiddleware extends Middleware
+ {
+ private function getToken(): string
+ {
+ $cookieName = $this->getProperty('cookieName', 'XSRF-TOKEN');
+ if (isset($_COOKIE[$cookieName])) {
+ $token = $_COOKIE[$cookieName];
+ } else {
+ $secure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on';
+ $token = bin2hex(random_bytes(8));
+ if (!headers_sent()) {
+ setcookie($cookieName, $token, 0, '', '', $secure);
+ }
+ }
+ return $token;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $token = $this->getToken();
+ $method = $request->getMethod();
+ $excludeMethods = $this->getArrayProperty('excludeMethods', 'OPTIONS,GET');
+ if (!in_array($method, $excludeMethods)) {
+ $headerName = $this->getProperty('headerName', 'X-XSRF-TOKEN');
+ if ($token != $request->getHeader($headerName)) {
+ return $this->responder->error(ErrorCode::BAD_OR_MISSING_XSRF_TOKEN, '');
+ }
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/OpenApi/OpenApiBuilder.php
+namespace Tqdev\PhpCrudApi\OpenApi {
+
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\OpenApi\OpenApiDefinition;
+
+ class OpenApiBuilder
+ {
+ private $openapi;
+ private $records;
+ private $columns;
+ private $builders;
+
+ public function __construct(ReflectionService $reflection, array $base, array $controllers, array $builders)
+ {
+ $this->openapi = new OpenApiDefinition($base);
+ $this->records = in_array('records', $controllers) ? new OpenApiRecordsBuilder($this->openapi, $reflection) : null;
+ $this->columns = in_array('columns', $controllers) ? new OpenApiColumnsBuilder($this->openapi) : null;
+ $this->builders = array();
+ foreach ($builders as $className) {
+ $this->builders[] = new $className($this->openapi, $reflection);
+ }
+ }
+
+ private function getServerUrl(): string
+ {
+ $protocol = @$_SERVER['HTTP_X_FORWARDED_PROTO'] ?: @$_SERVER['REQUEST_SCHEME'] ?: ((isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] == "on") ? "https" : "http");
+ $port = @intval($_SERVER['HTTP_X_FORWARDED_PORT']) ?: @intval($_SERVER["SERVER_PORT"]) ?: (($protocol === 'https') ? 443 : 80);
+ $host = @explode(":", $_SERVER['HTTP_HOST'])[0] ?: @$_SERVER['SERVER_NAME'] ?: @$_SERVER['SERVER_ADDR'];
+ $port = ($protocol === 'https' && $port === 443) || ($protocol === 'http' && $port === 80) ? '' : ':' . $port;
+ $path = @trim(substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], '/openapi')), '/');
+ return sprintf('%s://%s%s/%s', $protocol, $host, $port, $path);
+ }
+
+ public function build(): OpenApiDefinition
+ {
+ $this->openapi->set("openapi", "3.0.0");
+ if (!$this->openapi->has("servers") && isset($_SERVER['REQUEST_URI'])) {
+ $this->openapi->set("servers|0|url", $this->getServerUrl());
+ }
+ if ($this->records) {
+ $this->records->build();
+ }
+ if ($this->columns) {
+ $this->columns->build();
+ }
+ foreach ($this->builders as $builder) {
+ $builder->build();
+ }
+ return $this->openapi;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/OpenApi/OpenApiColumnsBuilder.php
+namespace Tqdev\PhpCrudApi\OpenApi {
+
+ use Tqdev\PhpCrudApi\OpenApi\OpenApiDefinition;
+
+ class OpenApiColumnsBuilder
+ {
+ private $openapi;
+ private $operations = [
+ 'database' => [
+ 'read' => 'get',
+ ],
+ 'table' => [
+ 'create' => 'post',
+ 'read' => 'get',
+ 'update' => 'put', //rename
+ 'delete' => 'delete',
+ ],
+ 'column' => [
+ 'create' => 'post',
+ 'read' => 'get',
+ 'update' => 'put',
+ 'delete' => 'delete',
+ ]
+ ];
+
+ public function __construct(OpenApiDefinition $openapi)
+ {
+ $this->openapi = $openapi;
+ }
+
+ public function build() /*: void*/
+ {
+ $this->setPaths();
+ $this->openapi->set("components|responses|boolSuccess|description", "boolean indicating success or failure");
+ $this->openapi->set("components|responses|boolSuccess|content|application/json|schema|type", "boolean");
+ $this->setComponentSchema();
+ $this->setComponentResponse();
+ $this->setComponentRequestBody();
+ $this->setComponentParameters();
+ foreach (array_keys($this->operations) as $index => $type) {
+ $this->setTag($index, $type);
+ }
+ }
+
+ private function setPaths() /*: void*/
+ {
+ foreach (array_keys($this->operations) as $type) {
+ foreach ($this->operations[$type] as $operation => $method) {
+ $parameters = [];
+ switch ($type) {
+ case 'database':
+ $path = '/columns';
+ break;
+ case 'table':
+ $path = $operation == 'create' ? '/columns' : '/columns/{table}';
+ break;
+ case 'column':
+ $path = $operation == 'create' ? '/columns/{table}' : '/columns/{table}/{column}';
+ break;
+ }
+ if (strpos($path, '{table}')) {
+ $parameters[] = 'table';
+ }
+ if (strpos($path, '{column}')) {
+ $parameters[] = 'column';
+ }
+ foreach ($parameters as $p => $parameter) {
+ $this->openapi->set("paths|$path|$method|parameters|$p|\$ref", "#/components/parameters/$parameter");
+ }
+ $operationType = $operation . ucfirst($type);
+ if (in_array($operation, ['create', 'update'])) {
+ $this->openapi->set("paths|$path|$method|requestBody|\$ref", "#/components/requestBodies/$operationType");
+ }
+ $this->openapi->set("paths|$path|$method|tags|0", "$type");
+ $this->openapi->set("paths|$path|$method|operationId", "$operation" . "_" . "$type");
+ if ($operationType == 'updateTable') {
+ $this->openapi->set("paths|$path|$method|description", "rename table");
+ } else {
+ $this->openapi->set("paths|$path|$method|description", "$operation $type");
+ }
+ switch ($operation) {
+ case 'read':
+ $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/$operationType");
+ break;
+ case 'create':
+ case 'update':
+ case 'delete':
+ $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/boolSuccess");
+ break;
+ }
+ }
+ }
+ }
+
+ private function setComponentSchema() /*: void*/
+ {
+ foreach (array_keys($this->operations) as $type) {
+ foreach (array_keys($this->operations[$type]) as $operation) {
+ if ($operation == 'delete') {
+ continue;
+ }
+ $operationType = $operation . ucfirst($type);
+ $prefix = "components|schemas|$operationType";
+ $this->openapi->set("$prefix|type", "object");
+ switch ($type) {
+ case 'database':
+ $this->openapi->set("$prefix|properties|tables|type", 'array');
+ $this->openapi->set("$prefix|properties|tables|items|\$ref", "#/components/schemas/readTable");
+ break;
+ case 'table':
+ if ($operation == 'update') {
+ $this->openapi->set("$prefix|required", ['name']);
+ $this->openapi->set("$prefix|properties|name|type", 'string');
+ } else {
+ $this->openapi->set("$prefix|properties|name|type", 'string');
+ if ($operation == 'read') {
+ $this->openapi->set("$prefix|properties|type|type", 'string');
+ }
+ $this->openapi->set("$prefix|properties|columns|type", 'array');
+ $this->openapi->set("$prefix|properties|columns|items|\$ref", "#/components/schemas/readColumn");
+ }
+ break;
+ case 'column':
+ $this->openapi->set("$prefix|required", ['name', 'type']);
+ $this->openapi->set("$prefix|properties|name|type", 'string');
+ $this->openapi->set("$prefix|properties|type|type", 'string');
+ $this->openapi->set("$prefix|properties|length|type", 'integer');
+ $this->openapi->set("$prefix|properties|length|format", "int64");
+ $this->openapi->set("$prefix|properties|precision|type", 'integer');
+ $this->openapi->set("$prefix|properties|precision|format", "int64");
+ $this->openapi->set("$prefix|properties|scale|type", 'integer');
+ $this->openapi->set("$prefix|properties|scale|format", "int64");
+ $this->openapi->set("$prefix|properties|nullable|type", 'boolean');
+ $this->openapi->set("$prefix|properties|pk|type", 'boolean');
+ $this->openapi->set("$prefix|properties|fk|type", 'string');
+ break;
+ }
+ }
+ }
+ }
+
+ private function setComponentResponse() /*: void*/
+ {
+ foreach (array_keys($this->operations) as $type) {
+ foreach (array_keys($this->operations[$type]) as $operation) {
+ if ($operation != 'read') {
+ continue;
+ }
+ $operationType = $operation . ucfirst($type);
+ $this->openapi->set("components|responses|$operationType|description", "single $type record");
+ $this->openapi->set("components|responses|$operationType|content|application/json|schema|\$ref", "#/components/schemas/$operationType");
+ }
+ }
+ }
+
+ private function setComponentRequestBody() /*: void*/
+ {
+ foreach (array_keys($this->operations) as $type) {
+ foreach (array_keys($this->operations[$type]) as $operation) {
+ if (!in_array($operation, ['create', 'update'])) {
+ continue;
+ }
+ $operationType = $operation . ucfirst($type);
+ $this->openapi->set("components|requestBodies|$operationType|description", "single $type record");
+ $this->openapi->set("components|requestBodies|$operationType|content|application/json|schema|\$ref", "#/components/schemas/$operationType");
+ }
+ }
+ }
+
+ private function setComponentParameters() /*: void*/
+ {
+ $this->openapi->set("components|parameters|table|name", "table");
+ $this->openapi->set("components|parameters|table|in", "path");
+ $this->openapi->set("components|parameters|table|schema|type", "string");
+ $this->openapi->set("components|parameters|table|description", "table name");
+ $this->openapi->set("components|parameters|table|required", true);
+
+ $this->openapi->set("components|parameters|column|name", "column");
+ $this->openapi->set("components|parameters|column|in", "path");
+ $this->openapi->set("components|parameters|column|schema|type", "string");
+ $this->openapi->set("components|parameters|column|description", "column name");
+ $this->openapi->set("components|parameters|column|required", true);
+ }
+
+ private function setTag(int $index, string $type) /*: void*/
+ {
+ $this->openapi->set("tags|$index|name", "$type");
+ $this->openapi->set("tags|$index|description", "$type operations");
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/OpenApi/OpenApiDefinition.php
+namespace Tqdev\PhpCrudApi\OpenApi {
+
+ class OpenApiDefinition implements \JsonSerializable
+ {
+ private $root;
+
+ public function __construct(array $base)
+ {
+ $this->root = $base;
+ }
+
+ public function set(string $path, $value) /*: void*/
+ {
+ $parts = explode('|', trim($path, '|'));
+ $current = &$this->root;
+ while (count($parts) > 0) {
+ $part = array_shift($parts);
+ if (!isset($current[$part])) {
+ $current[$part] = [];
+ }
+ $current = &$current[$part];
+ }
+ $current = $value;
+ }
+
+ public function has(string $path): bool
+ {
+ $parts = explode('|', trim($path, '|'));
+ $current = &$this->root;
+ while (count($parts) > 0) {
+ $part = array_shift($parts);
+ if (!isset($current[$part])) {
+ return false;
+ }
+ $current = &$current[$part];
+ }
+ return true;
+ }
+
+ public function jsonSerialize()
+ {
+ return $this->root;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/OpenApi/OpenApiRecordsBuilder.php
+namespace Tqdev\PhpCrudApi\OpenApi {
+
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
+ use Tqdev\PhpCrudApi\Middleware\Communication\VariableStore;
+ use Tqdev\PhpCrudApi\OpenApi\OpenApiDefinition;
+
+ class OpenApiRecordsBuilder
+ {
+ private $openapi;
+ private $reflection;
+ private $operations = [
+ 'list' => 'get',
+ 'create' => 'post',
+ 'read' => 'get',
+ 'update' => 'put',
+ 'delete' => 'delete',
+ 'increment' => 'patch',
+ ];
+ private $types = [
+ 'integer' => ['type' => 'integer', 'format' => 'int32'],
+ 'bigint' => ['type' => 'integer', 'format' => 'int64'],
+ 'varchar' => ['type' => 'string'],
+ 'clob' => ['type' => 'string', 'format' => 'large-string'], //custom format
+ 'varbinary' => ['type' => 'string', 'format' => 'byte'],
+ 'blob' => ['type' => 'string', 'format' => 'large-byte'], //custom format
+ 'decimal' => ['type' => 'string', 'format' => 'decimal'], //custom format
+ 'float' => ['type' => 'number', 'format' => 'float'],
+ 'double' => ['type' => 'number', 'format' => 'double'],
+ 'date' => ['type' => 'string', 'format' => 'date'],
+ 'time' => ['type' => 'string', 'format' => 'time'], //custom format
+ 'timestamp' => ['type' => 'string', 'format' => 'date-time'],
+ 'geometry' => ['type' => 'string', 'format' => 'geometry'], //custom format
+ 'boolean' => ['type' => 'boolean'],
+ ];
+
+ public function __construct(OpenApiDefinition $openapi, ReflectionService $reflection)
+ {
+ $this->openapi = $openapi;
+ $this->reflection = $reflection;
+ }
+
+ private function getAllTableReferences(): array
+ {
+ $tableReferences = array();
+ foreach ($this->reflection->getTableNames() as $tableName) {
+ $table = $this->reflection->getTable($tableName);
+ foreach ($table->getColumnNames() as $columnName) {
+ $column = $table->getColumn($columnName);
+ $referencedTableName = $column->getFk();
+ if ($referencedTableName) {
+ if (!isset($tableReferences[$referencedTableName])) {
+ $tableReferences[$referencedTableName] = array();
+ }
+ $tableReferences[$referencedTableName][] = "$tableName.$columnName";
+ }
+ }
+ }
+ return $tableReferences;
+ }
+
+ public function build() /*: void*/
+ {
+ $tableNames = $this->reflection->getTableNames();
+ foreach ($tableNames as $tableName) {
+ $this->setPath($tableName);
+ }
+ $this->openapi->set("components|responses|pk_integer|description", "inserted primary key value (integer)");
+ $this->openapi->set("components|responses|pk_integer|content|application/json|schema|type", "integer");
+ $this->openapi->set("components|responses|pk_integer|content|application/json|schema|format", "int64");
+ $this->openapi->set("components|responses|pk_string|description", "inserted primary key value (string)");
+ $this->openapi->set("components|responses|pk_string|content|application/json|schema|type", "string");
+ $this->openapi->set("components|responses|pk_string|content|application/json|schema|format", "uuid");
+ $this->openapi->set("components|responses|rows_affected|description", "number of rows affected (integer)");
+ $this->openapi->set("components|responses|rows_affected|content|application/json|schema|type", "integer");
+ $this->openapi->set("components|responses|rows_affected|content|application/json|schema|format", "int64");
+ $tableReferences = $this->getAllTableReferences();
+ foreach ($tableNames as $tableName) {
+ $references = isset($tableReferences[$tableName]) ? $tableReferences[$tableName] : array();
+ $this->setComponentSchema($tableName, $references);
+ $this->setComponentResponse($tableName);
+ $this->setComponentRequestBody($tableName);
+ }
+ $this->setComponentParameters();
+ foreach ($tableNames as $index => $tableName) {
+ $this->setTag($index, $tableName);
+ }
+ }
+
+ private function isOperationOnTableAllowed(string $operation, string $tableName): bool
+ {
+ $tableHandler = VariableStore::get('authorization.tableHandler');
+ if (!$tableHandler) {
+ return true;
+ }
+ return (bool) call_user_func($tableHandler, $operation, $tableName);
+ }
+
+ private function isOperationOnColumnAllowed(string $operation, string $tableName, string $columnName): bool
+ {
+ $columnHandler = VariableStore::get('authorization.columnHandler');
+ if (!$columnHandler) {
+ return true;
+ }
+ return (bool) call_user_func($columnHandler, $operation, $tableName, $columnName);
+ }
+
+ private function setPath(string $tableName) /*: void*/
+ {
+ $table = $this->reflection->getTable($tableName);
+ $type = $table->getType();
+ $pk = $table->getPk();
+ $pkName = $pk ? $pk->getName() : '';
+ foreach ($this->operations as $operation => $method) {
+ if (!$pkName && $operation != 'list') {
+ continue;
+ }
+ if ($type != 'table' && $operation != 'list') {
+ continue;
+ }
+ if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
+ continue;
+ }
+ $parameters = [];
+ if (in_array($operation, ['list', 'create'])) {
+ $path = sprintf('/records/%s', $tableName);
+ if ($operation == 'list') {
+ $parameters = ['filter', 'include', 'exclude', 'order', 'size', 'page', 'join'];
+ }
+ } else {
+ $path = sprintf('/records/%s/{id}', $tableName);
+ if ($operation == 'read') {
+ $parameters = ['pk', 'include', 'exclude', 'join'];
+ } else {
+ $parameters = ['pk'];
+ }
+ }
+ foreach ($parameters as $p => $parameter) {
+ $this->openapi->set("paths|$path|$method|parameters|$p|\$ref", "#/components/parameters/$parameter");
+ }
+ if (in_array($operation, ['create', 'update', 'increment'])) {
+ $this->openapi->set("paths|$path|$method|requestBody|\$ref", "#/components/requestBodies/$operation-" . rawurlencode($tableName));
+ }
+ $this->openapi->set("paths|$path|$method|tags|0", "$tableName");
+ $this->openapi->set("paths|$path|$method|operationId", "$operation" . "_" . "$tableName");
+ $this->openapi->set("paths|$path|$method|description", "$operation $tableName");
+ switch ($operation) {
+ case 'list':
+ $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/$operation-" . rawurlencode($tableName));
+ break;
+ case 'create':
+ if ($pk->getType() == 'integer') {
+ $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/pk_integer");
+ } else {
+ $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/pk_string");
+ }
+ break;
+ case 'read':
+ $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/$operation-" . rawurlencode($tableName));
+ break;
+ case 'update':
+ case 'delete':
+ case 'increment':
+ $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/rows_affected");
+ break;
+ }
+ }
+ }
+
+ private function getPattern(ReflectedColumn $column): string
+ {
+ switch ($column->getType()) {
+ case 'integer':
+ $n = strlen(pow(2, 31));
+ return '^-?[0-9]{1,' . $n . '}$';
+ case 'bigint':
+ $n = strlen(pow(2, 63));
+ return '^-?[0-9]{1,' . $n . '}$';
+ case 'varchar':
+ $l = $column->getLength();
+ return '^.{0,' . $l . '}$';
+ case 'clob':
+ return '^.*$';
+ case 'varbinary':
+ $l = $column->getLength();
+ $b = (int) 4 * ceil($l / 3);
+ return '^[A-Za-z0-9+/]{0,' . $b . '}=*$';
+ case 'blob':
+ return '^[A-Za-z0-9+/]*=*$';
+ case 'decimal':
+ $p = $column->getPrecision();
+ $s = $column->getScale();
+ return '^-?[0-9]{1,' . ($p - $s) . '}(\.[0-9]{1,' . $s . '})?$';
+ case 'float':
+ return '^-?[0-9]+(\.[0-9]+)?([eE]-?[0-9]+)?$';
+ case 'double':
+ return '^-?[0-9]+(\.[0-9]+)?([eE]-?[0-9]+)?$';
+ case 'date':
+ return '^[0-9]{4}-[0-9]{2}-[0-9]{2}$';
+ case 'time':
+ return '^[0-9]{2}:[0-9]{2}:[0-9]{2}$';
+ case 'timestamp':
+ return '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$';
+ return '';
+ case 'geometry':
+ return '^(POINT|LINESTRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON)\s*\(.*$';
+ case 'boolean':
+ return '^(true|false)$';
+ }
+ return '';
+ }
+
+ private function setComponentSchema(string $tableName, array $references) /*: void*/
+ {
+ $table = $this->reflection->getTable($tableName);
+ $type = $table->getType();
+ $pk = $table->getPk();
+ $pkName = $pk ? $pk->getName() : '';
+ foreach ($this->operations as $operation => $method) {
+ if (!$pkName && $operation != 'list') {
+ continue;
+ }
+ if ($type == 'view' && !in_array($operation, array('read', 'list'))) {
+ continue;
+ }
+ if ($type == 'view' && !$pkName && $operation == 'read') {
+ continue;
+ }
+ if ($operation == 'delete') {
+ continue;
+ }
+ if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
+ continue;
+ }
+ if ($operation == 'list') {
+ $this->openapi->set("components|schemas|$operation-$tableName|type", "object");
+ $this->openapi->set("components|schemas|$operation-$tableName|properties|results|type", "integer");
+ $this->openapi->set("components|schemas|$operation-$tableName|properties|results|format", "int64");
+ $this->openapi->set("components|schemas|$operation-$tableName|properties|records|type", "array");
+ $prefix = "components|schemas|$operation-$tableName|properties|records|items";
+ } else {
+ $prefix = "components|schemas|$operation-$tableName";
+ }
+ $this->openapi->set("$prefix|type", "object");
+ foreach ($table->getColumnNames() as $columnName) {
+ if (!$this->isOperationOnColumnAllowed($operation, $tableName, $columnName)) {
+ continue;
+ }
+ $column = $table->getColumn($columnName);
+ $properties = $this->types[$column->getType()];
+ $properties['maxLength'] = $column->hasLength() ? $column->getLength() : 0;
+ $properties['nullable'] = $column->getNullable();
+ $properties['pattern'] = $this->getPattern($column);
+ foreach ($properties as $key => $value) {
+ if ($value) {
+ $this->openapi->set("$prefix|properties|$columnName|$key", $value);
+ }
+ }
+ if ($column->getPk()) {
+ $this->openapi->set("$prefix|properties|$columnName|x-primary-key", true);
+ $this->openapi->set("$prefix|properties|$columnName|x-referenced", $references);
+ }
+ $fk = $column->getFk();
+ if ($fk) {
+ $this->openapi->set("$prefix|properties|$columnName|x-references", $fk);
+ }
+ }
+ }
+ }
+
+ private function setComponentResponse(string $tableName) /*: void*/
+ {
+ $table = $this->reflection->getTable($tableName);
+ $type = $table->getType();
+ $pk = $table->getPk();
+ $pkName = $pk ? $pk->getName() : '';
+ foreach (['list', 'read'] as $operation) {
+ if (!$pkName && $operation != 'list') {
+ continue;
+ }
+ if ($type != 'table' && $operation != 'list') {
+ continue;
+ }
+ if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
+ continue;
+ }
+ if ($operation == 'list') {
+ $this->openapi->set("components|responses|$operation-$tableName|description", "list of $tableName records");
+ } else {
+ $this->openapi->set("components|responses|$operation-$tableName|description", "single $tableName record");
+ }
+ $this->openapi->set("components|responses|$operation-$tableName|content|application/json|schema|\$ref", "#/components/schemas/$operation-" . rawurlencode($tableName));
+ }
+ }
+
+ private function setComponentRequestBody(string $tableName) /*: void*/
+ {
+ $table = $this->reflection->getTable($tableName);
+ $type = $table->getType();
+ $pk = $table->getPk();
+ $pkName = $pk ? $pk->getName() : '';
+ if ($pkName && $type == 'table') {
+ foreach (['create', 'update', 'increment'] as $operation) {
+ if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
+ continue;
+ }
+ $this->openapi->set("components|requestBodies|$operation-$tableName|description", "single $tableName record");
+ $this->openapi->set("components|requestBodies|$operation-$tableName|content|application/json|schema|\$ref", "#/components/schemas/$operation-" . rawurlencode($tableName));
+ }
+ }
+ }
+
+ private function setComponentParameters() /*: void*/
+ {
+ $this->openapi->set("components|parameters|pk|name", "id");
+ $this->openapi->set("components|parameters|pk|in", "path");
+ $this->openapi->set("components|parameters|pk|schema|type", "string");
+ $this->openapi->set("components|parameters|pk|description", "primary key value");
+ $this->openapi->set("components|parameters|pk|required", true);
+
+ $this->openapi->set("components|parameters|filter|name", "filter");
+ $this->openapi->set("components|parameters|filter|in", "query");
+ $this->openapi->set("components|parameters|filter|schema|type", "array");
+ $this->openapi->set("components|parameters|filter|schema|items|type", "string");
+ $this->openapi->set("components|parameters|filter|description", "Filters to be applied. Each filter consists of a column, an operator and a value (comma separated). Example: id,eq,1");
+ $this->openapi->set("components|parameters|filter|required", false);
+
+ $this->openapi->set("components|parameters|include|name", "include");
+ $this->openapi->set("components|parameters|include|in", "query");
+ $this->openapi->set("components|parameters|include|schema|type", "string");
+ $this->openapi->set("components|parameters|include|description", "Columns you want to include in the output (comma separated). Example: posts.*,categories.name");
+ $this->openapi->set("components|parameters|include|required", false);
+
+ $this->openapi->set("components|parameters|exclude|name", "exclude");
+ $this->openapi->set("components|parameters|exclude|in", "query");
+ $this->openapi->set("components|parameters|exclude|schema|type", "string");
+ $this->openapi->set("components|parameters|exclude|description", "Columns you want to exclude from the output (comma separated). Example: posts.content");
+ $this->openapi->set("components|parameters|exclude|required", false);
+
+ $this->openapi->set("components|parameters|order|name", "order");
+ $this->openapi->set("components|parameters|order|in", "query");
+ $this->openapi->set("components|parameters|order|schema|type", "array");
+ $this->openapi->set("components|parameters|order|schema|items|type", "string");
+ $this->openapi->set("components|parameters|order|description", "Column you want to sort on and the sort direction (comma separated). Example: id,desc");
+ $this->openapi->set("components|parameters|order|required", false);
+
+ $this->openapi->set("components|parameters|size|name", "size");
+ $this->openapi->set("components|parameters|size|in", "query");
+ $this->openapi->set("components|parameters|size|schema|type", "string");
+ $this->openapi->set("components|parameters|size|description", "Maximum number of results (for top lists). Example: 10");
+ $this->openapi->set("components|parameters|size|required", false);
+
+ $this->openapi->set("components|parameters|page|name", "page");
+ $this->openapi->set("components|parameters|page|in", "query");
+ $this->openapi->set("components|parameters|page|schema|type", "string");
+ $this->openapi->set("components|parameters|page|description", "Page number and page size (comma separated). Example: 1,10");
+ $this->openapi->set("components|parameters|page|required", false);
+
+ $this->openapi->set("components|parameters|join|name", "join");
+ $this->openapi->set("components|parameters|join|in", "query");
+ $this->openapi->set("components|parameters|join|schema|type", "array");
+ $this->openapi->set("components|parameters|join|schema|items|type", "string");
+ $this->openapi->set("components|parameters|join|description", "Paths (comma separated) to related entities that you want to include. Example: comments,users");
+ $this->openapi->set("components|parameters|join|required", false);
+ }
+
+ private function setTag(int $index, string $tableName) /*: void*/
+ {
+ $this->openapi->set("tags|$index|name", "$tableName");
+ $this->openapi->set("tags|$index|description", "$tableName operations");
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/OpenApi/OpenApiService.php
+namespace Tqdev\PhpCrudApi\OpenApi {
+
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\OpenApi\OpenApiBuilder;
+
+ class OpenApiService
+ {
+ private $builder;
+
+ public function __construct(ReflectionService $reflection, array $base, array $controllers, array $customBuilders)
+ {
+ $this->builder = new OpenApiBuilder($reflection, $base, $controllers, $customBuilders);
+ }
+
+ public function get(): OpenApiDefinition
+ {
+ return $this->builder->build();
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/Condition/AndCondition.php
+namespace Tqdev\PhpCrudApi\Record\Condition {
+
+ class AndCondition extends Condition
+ {
+ private $conditions;
+
+ public function __construct(Condition $condition1, Condition $condition2)
+ {
+ $this->conditions = [$condition1, $condition2];
+ }
+
+ public function _and(Condition $condition): Condition
+ {
+ if ($condition instanceof NoCondition) {
+ return $this;
+ }
+ $this->conditions[] = $condition;
+ return $this;
+ }
+
+ public function getConditions(): array
+ {
+ return $this->conditions;
+ }
+
+ public static function fromArray(array $conditions): Condition
+ {
+ $condition = new NoCondition();
+ foreach ($conditions as $c) {
+ $condition = $condition->_and($c);
+ }
+ return $condition;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/Condition/ColumnCondition.php
+namespace Tqdev\PhpCrudApi\Record\Condition {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
+
+ class ColumnCondition extends Condition
+ {
+ private $column;
+ private $operator;
+ private $value;
+
+ public function __construct(ReflectedColumn $column, string $operator, string $value)
+ {
+ $this->column = $column;
+ $this->operator = $operator;
+ $this->value = $value;
+ }
+
+ public function getColumn(): ReflectedColumn
+ {
+ return $this->column;
+ }
+
+ public function getOperator(): string
+ {
+ return $this->operator;
+ }
+
+ public function getValue(): string
+ {
+ return $this->value;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/Condition/Condition.php
+namespace Tqdev\PhpCrudApi\Record\Condition {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+
+ abstract class Condition
+ {
+ public function _and(Condition $condition): Condition
+ {
+ if ($condition instanceof NoCondition) {
+ return $this;
+ }
+ return new AndCondition($this, $condition);
+ }
+
+ public function _or(Condition $condition): Condition
+ {
+ if ($condition instanceof NoCondition) {
+ return $this;
+ }
+ return new OrCondition($this, $condition);
+ }
+
+ public function _not(): Condition
+ {
+ return new NotCondition($this);
+ }
+
+ public static function fromString(ReflectedTable $table, string $value): Condition
+ {
+ $condition = new NoCondition();
+ $parts = explode(',', $value, 3);
+ if (count($parts) < 2) {
+ return $condition;
+ }
+ if (count($parts) < 3) {
+ $parts[2] = '';
+ }
+ $field = $table->getColumn($parts[0]);
+ $command = $parts[1];
+ $negate = false;
+ $spatial = false;
+ if (strlen($command) > 2) {
+ if (substr($command, 0, 1) == 'n') {
+ $negate = true;
+ $command = substr($command, 1);
+ }
+ if (substr($command, 0, 1) == 's') {
+ $spatial = true;
+ $command = substr($command, 1);
+ }
+ }
+ if ($spatial) {
+ if (in_array($command, ['co', 'cr', 'di', 'eq', 'in', 'ov', 'to', 'wi', 'ic', 'is', 'iv'])) {
+ $condition = new SpatialCondition($field, $command, $parts[2]);
+ }
+ } else {
+ if (in_array($command, ['cs', 'sw', 'ew', 'eq', 'lt', 'le', 'ge', 'gt', 'bt', 'in', 'is'])) {
+ $condition = new ColumnCondition($field, $command, $parts[2]);
+ }
+ }
+ if ($negate) {
+ $condition = $condition->_not();
+ }
+ return $condition;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/Condition/NoCondition.php
+namespace Tqdev\PhpCrudApi\Record\Condition {
+
+ class NoCondition extends Condition
+ {
+ public function _and(Condition $condition): Condition
+ {
+ return $condition;
+ }
+
+ public function _or(Condition $condition): Condition
+ {
+ return $condition;
+ }
+
+ public function _not(): Condition
+ {
+ return $this;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/Condition/NotCondition.php
+namespace Tqdev\PhpCrudApi\Record\Condition {
+
+ class NotCondition extends Condition
+ {
+ private $condition;
+
+ public function __construct(Condition $condition)
+ {
+ $this->condition = $condition;
+ }
+
+ public function getCondition(): Condition
+ {
+ return $this->condition;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/Condition/OrCondition.php
+namespace Tqdev\PhpCrudApi\Record\Condition {
+
+ class OrCondition extends Condition
+ {
+ private $conditions;
+
+ public function __construct(Condition $condition1, Condition $condition2)
+ {
+ $this->conditions = [$condition1, $condition2];
+ }
+
+ public function _or(Condition $condition): Condition
+ {
+ if ($condition instanceof NoCondition) {
+ return $this;
+ }
+ $this->conditions[] = $condition;
+ return $this;
+ }
+
+ public function getConditions(): array
+ {
+ return $this->conditions;
+ }
+
+ public static function fromArray(array $conditions): Condition
+ {
+ $condition = new NoCondition();
+ foreach ($conditions as $c) {
+ $condition = $condition->_or($c);
+ }
+ return $condition;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/Condition/SpatialCondition.php
+namespace Tqdev\PhpCrudApi\Record\Condition {
+
+ class SpatialCondition extends ColumnCondition
+ {
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/Document/ErrorDocument.php
+namespace Tqdev\PhpCrudApi\Record\Document {
+
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+
+ class ErrorDocument implements \JsonSerializable
+ {
+ public $code;
+ public $message;
+ public $details;
+
+ public function __construct(ErrorCode $errorCode, string $argument, $details)
+ {
+ $this->code = $errorCode->getCode();
+ $this->message = $errorCode->getMessage($argument);
+ $this->details = $details;
+ }
+
+ public function getCode(): int
+ {
+ return $this->code;
+ }
+
+ public function getMessage(): string
+ {
+ return $this->message;
+ }
+
+ public function serialize()
+ {
+ return [
+ 'code' => $this->code,
+ 'message' => $this->message,
+ 'details' => $this->details,
+ ];
+ }
+
+ public function jsonSerialize()
+ {
+ return array_filter($this->serialize());
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/Document/ListDocument.php
+namespace Tqdev\PhpCrudApi\Record\Document {
+
+ class ListDocument implements \JsonSerializable
+ {
+ private $records;
+
+ private $results;
+
+ public function __construct(array $records, int $results)
+ {
+ $this->records = $records;
+ $this->results = $results;
+ }
+
+ public function getRecords(): array
+ {
+ return $this->records;
+ }
+
+ public function getResults(): int
+ {
+ return $this->results;
+ }
+
+ public function serialize()
+ {
+ return [
+ 'records' => $this->records,
+ 'results' => $this->results,
+ ];
+ }
+
+ public function jsonSerialize()
+ {
+ return array_filter($this->serialize(), function ($v) {
+ return $v !== 0;
+ });
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/ColumnIncluder.php
+namespace Tqdev\PhpCrudApi\Record {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+
+ class ColumnIncluder
+ {
+ private function isMandatory(string $tableName, string $columnName, array $params): bool
+ {
+ return isset($params['mandatory']) && in_array($tableName . "." . $columnName, $params['mandatory']);
+ }
+
+ private function select(
+ string $tableName,
+ bool $primaryTable,
+ array $params,
+ string $paramName,
+ array $columnNames,
+ bool $include
+ ): array {
+ if (!isset($params[$paramName])) {
+ return $columnNames;
+ }
+ $columns = array();
+ foreach (explode(',', $params[$paramName][0]) as $columnName) {
+ $columns[$columnName] = true;
+ }
+ $result = array();
+ foreach ($columnNames as $columnName) {
+ $match = isset($columns['*.*']);
+ if (!$match) {
+ $match = isset($columns[$tableName . '.*']) || isset($columns[$tableName . '.' . $columnName]);
+ }
+ if ($primaryTable && !$match) {
+ $match = isset($columns['*']) || isset($columns[$columnName]);
+ }
+ if ($match) {
+ if ($include || $this->isMandatory($tableName, $columnName, $params)) {
+ $result[] = $columnName;
+ }
+ } else {
+ if (!$include || $this->isMandatory($tableName, $columnName, $params)) {
+ $result[] = $columnName;
+ }
+ }
+ }
+ return $result;
+ }
+
+ public function getNames(ReflectedTable $table, bool $primaryTable, array $params): array
+ {
+ $tableName = $table->getName();
+ $results = $table->getColumnNames();
+ $results = $this->select($tableName, $primaryTable, $params, 'include', $results, true);
+ $results = $this->select($tableName, $primaryTable, $params, 'exclude', $results, false);
+ return $results;
+ }
+
+ public function getValues(ReflectedTable $table, bool $primaryTable, /* object */ $record, array $params): array
+ {
+ $results = array();
+ $columnNames = $this->getNames($table, $primaryTable, $params);
+ foreach ($columnNames as $columnName) {
+ if (property_exists($record, $columnName)) {
+ $results[$columnName] = $record->$columnName;
+ }
+ }
+ return $results;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/ErrorCode.php
+namespace Tqdev\PhpCrudApi\Record {
+
+ use Tqdev\PhpCrudApi\ResponseFactory;
+
+ class ErrorCode
+ {
+ private $code;
+ private $message;
+ private $status;
+
+ const ERROR_NOT_FOUND = 9999;
+ const ROUTE_NOT_FOUND = 1000;
+ const TABLE_NOT_FOUND = 1001;
+ const ARGUMENT_COUNT_MISMATCH = 1002;
+ const RECORD_NOT_FOUND = 1003;
+ const ORIGIN_FORBIDDEN = 1004;
+ const COLUMN_NOT_FOUND = 1005;
+ const TABLE_ALREADY_EXISTS = 1006;
+ const COLUMN_ALREADY_EXISTS = 1007;
+ const HTTP_MESSAGE_NOT_READABLE = 1008;
+ const DUPLICATE_KEY_EXCEPTION = 1009;
+ const DATA_INTEGRITY_VIOLATION = 1010;
+ const AUTHENTICATION_REQUIRED = 1011;
+ const AUTHENTICATION_FAILED = 1012;
+ const INPUT_VALIDATION_FAILED = 1013;
+ const OPERATION_FORBIDDEN = 1014;
+ const OPERATION_NOT_SUPPORTED = 1015;
+ const TEMPORARY_OR_PERMANENTLY_BLOCKED = 1016;
+ const BAD_OR_MISSING_XSRF_TOKEN = 1017;
+ const ONLY_AJAX_REQUESTS_ALLOWED = 1018;
+ const PAGINATION_FORBIDDEN = 1019;
+ const USER_ALREADY_EXIST = 1020;
+ const PASSWORD_TOO_SHORT = 1021;
+
+ private $values = [
+ 9999 => ["%s", ResponseFactory::INTERNAL_SERVER_ERROR],
+ 1000 => ["Route '%s' not found", ResponseFactory::NOT_FOUND],
+ 1001 => ["Table '%s' not found", ResponseFactory::NOT_FOUND],
+ 1002 => ["Argument count mismatch in '%s'", ResponseFactory::UNPROCESSABLE_ENTITY],
+ 1003 => ["Record '%s' not found", ResponseFactory::NOT_FOUND],
+ 1004 => ["Origin '%s' is forbidden", ResponseFactory::FORBIDDEN],
+ 1005 => ["Column '%s' not found", ResponseFactory::NOT_FOUND],
+ 1006 => ["Table '%s' already exists", ResponseFactory::CONFLICT],
+ 1007 => ["Column '%s' already exists", ResponseFactory::CONFLICT],
+ 1008 => ["Cannot read HTTP message", ResponseFactory::UNPROCESSABLE_ENTITY],
+ 1009 => ["Duplicate key exception", ResponseFactory::CONFLICT],
+ 1010 => ["Data integrity violation", ResponseFactory::CONFLICT],
+ 1011 => ["Authentication required", ResponseFactory::UNAUTHORIZED],
+ 1012 => ["Authentication failed for '%s'", ResponseFactory::FORBIDDEN],
+ 1013 => ["Input validation failed for '%s'", ResponseFactory::UNPROCESSABLE_ENTITY],
+ 1014 => ["Operation forbidden", ResponseFactory::FORBIDDEN],
+ 1015 => ["Operation '%s' not supported", ResponseFactory::METHOD_NOT_ALLOWED],
+ 1016 => ["Temporary or permanently blocked", ResponseFactory::FORBIDDEN],
+ 1017 => ["Bad or missing XSRF token", ResponseFactory::FORBIDDEN],
+ 1018 => ["Only AJAX requests allowed for '%s'", ResponseFactory::FORBIDDEN],
+ 1019 => ["Pagination forbidden", ResponseFactory::FORBIDDEN],
+ 1020 => ["User '%s' already exists", ResponseFactory::CONFLICT],
+ 1021 => ["Password too short (<%d characters)", ResponseFactory::UNPROCESSABLE_ENTITY],
+ ];
+
+ public function __construct(int $code)
+ {
+ if (!isset($this->values[$code])) {
+ $code = 9999;
+ }
+ $this->code = $code;
+ $this->message = $this->values[$code][0];
+ $this->status = $this->values[$code][1];
+ }
+
+ public function getCode(): int
+ {
+ return $this->code;
+ }
+
+ public function getMessage(string $argument): string
+ {
+ return sprintf($this->message, $argument);
+ }
+
+ public function getStatus(): int
+ {
+ return $this->status;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/FilterInfo.php
+namespace Tqdev\PhpCrudApi\Record {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+ use Tqdev\PhpCrudApi\Record\Condition\AndCondition;
+ use Tqdev\PhpCrudApi\Record\Condition\Condition;
+ use Tqdev\PhpCrudApi\Record\Condition\NoCondition;
+ use Tqdev\PhpCrudApi\Record\Condition\OrCondition;
+
+ class FilterInfo
+ {
+ private function getConditionsAsPathTree(ReflectedTable $table, array $params): PathTree
+ {
+ $conditions = new PathTree();
+ foreach ($params as $key => $filters) {
+ if (substr($key, 0, 6) == 'filter') {
+ preg_match_all('/\d+|\D+/', substr($key, 6), $matches);
+ $path = $matches[0];
+ foreach ($filters as $filter) {
+ $condition = Condition::fromString($table, $filter);
+ if (($condition instanceof NoCondition) == false) {
+ $conditions->put($path, $condition);
+ }
+ }
+ }
+ }
+ return $conditions;
+ }
+
+ private function combinePathTreeOfConditions(PathTree $tree): Condition
+ {
+ $andConditions = $tree->getValues();
+ $and = AndCondition::fromArray($andConditions);
+ $orConditions = [];
+ foreach ($tree->getKeys() as $p) {
+ $orConditions[] = $this->combinePathTreeOfConditions($tree->get($p));
+ }
+ $or = OrCondition::fromArray($orConditions);
+ return $and->_and($or);
+ }
+
+ public function getCombinedConditions(ReflectedTable $table, array $params): Condition
+ {
+ return $this->combinePathTreeOfConditions($this->getConditionsAsPathTree($table, $params));
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/HabtmValues.php
+namespace Tqdev\PhpCrudApi\Record {
+
+ class HabtmValues
+ {
+ public $pkValues;
+ public $fkValues;
+
+ public function __construct(array $pkValues, array $fkValues)
+ {
+ $this->pkValues = $pkValues;
+ $this->fkValues = $fkValues;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/OrderingInfo.php
+namespace Tqdev\PhpCrudApi\Record {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+
+ class OrderingInfo
+ {
+ public function getColumnOrdering(ReflectedTable $table, array $params): array
+ {
+ $fields = array();
+ if (isset($params['order'])) {
+ foreach ($params['order'] as $order) {
+ $parts = explode(',', $order, 3);
+ $columnName = $parts[0];
+ if (!$table->hasColumn($columnName)) {
+ continue;
+ }
+ $ascending = 'ASC';
+ if (count($parts) > 1) {
+ if (substr(strtoupper($parts[1]), 0, 4) == "DESC") {
+ $ascending = 'DESC';
+ }
+ }
+ $fields[] = [$columnName, $ascending];
+ }
+ }
+ if (count($fields) == 0) {
+ return $this->getDefaultColumnOrdering($table);
+ }
+ return $fields;
+ }
+
+ public function getDefaultColumnOrdering(ReflectedTable $table): array
+ {
+ $fields = array();
+ $pk = $table->getPk();
+ if ($pk) {
+ $fields[] = [$pk->getName(), 'ASC'];
+ } else {
+ foreach ($table->getColumnNames() as $columnName) {
+ $fields[] = [$columnName, 'ASC'];
+ }
+ }
+ return $fields;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/PaginationInfo.php
+namespace Tqdev\PhpCrudApi\Record {
+
+ class PaginationInfo
+ {
+ public $DEFAULT_PAGE_SIZE = 20;
+
+ public function hasPage(array $params): bool
+ {
+ return isset($params['page']);
+ }
+
+ public function getPageOffset(array $params): int
+ {
+ $offset = 0;
+ $pageSize = $this->getPageSize($params);
+ if (isset($params['page'])) {
+ foreach ($params['page'] as $page) {
+ $parts = explode(',', $page, 2);
+ $page = intval($parts[0]) - 1;
+ $offset = $page * $pageSize;
+ }
+ }
+ return $offset;
+ }
+
+ private function getPageSize(array $params): int
+ {
+ $pageSize = $this->DEFAULT_PAGE_SIZE;
+ if (isset($params['page'])) {
+ foreach ($params['page'] as $page) {
+ $parts = explode(',', $page, 2);
+ if (count($parts) > 1) {
+ $pageSize = intval($parts[1]);
+ }
+ }
+ }
+ return $pageSize;
+ }
+
+ public function getResultSize(array $params): int
+ {
+ $numberOfRows = -1;
+ if (isset($params['size'])) {
+ foreach ($params['size'] as $size) {
+ $numberOfRows = intval($size);
+ }
+ }
+ return $numberOfRows;
+ }
+
+ public function getPageLimit(array $params): int
+ {
+ $pageLimit = -1;
+ if ($this->hasPage($params)) {
+ $pageLimit = $this->getPageSize($params);
+ }
+ $resultSize = $this->getResultSize($params);
+ if ($resultSize >= 0) {
+ if ($pageLimit >= 0) {
+ $pageLimit = min($pageLimit, $resultSize);
+ } else {
+ $pageLimit = $resultSize;
+ }
+ }
+ return $pageLimit;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/PathTree.php
+namespace Tqdev\PhpCrudApi\Record {
+
+ class PathTree implements \JsonSerializable
+ {
+ const WILDCARD = '*';
+
+ private $tree;
+
+ public function __construct(/* object */&$tree = null)
+ {
+ if (!$tree) {
+ $tree = $this->newTree();
+ }
+ $this->tree = &$tree;
+ }
+
+ public function newTree()
+ {
+ return (object) ['values' => [], 'branches' => (object) []];
+ }
+
+ public function getKeys(): array
+ {
+ $branches = (array) $this->tree->branches;
+ return array_keys($branches);
+ }
+
+ public function getValues(): array
+ {
+ return $this->tree->values;
+ }
+
+ public function get(string $key): PathTree
+ {
+ if (!isset($this->tree->branches->$key)) {
+ return null;
+ }
+ return new PathTree($this->tree->branches->$key);
+ }
+
+ public function put(array $path, $value)
+ {
+ $tree = &$this->tree;
+ foreach ($path as $key) {
+ if (!isset($tree->branches->$key)) {
+ $tree->branches->$key = $this->newTree();
+ }
+ $tree = &$tree->branches->$key;
+ }
+ $tree->values[] = $value;
+ }
+
+ public function match(array $path): array
+ {
+ $star = self::WILDCARD;
+ $tree = &$this->tree;
+ foreach ($path as $key) {
+ if (isset($tree->branches->$key)) {
+ $tree = &$tree->branches->$key;
+ } elseif (isset($tree->branches->$star)) {
+ $tree = &$tree->branches->$star;
+ } else {
+ return [];
+ }
+ }
+ return $tree->values;
+ }
+
+ public static function fromJson(/* object */$tree): PathTree
+ {
+ return new PathTree($tree);
+ }
+
+ public function jsonSerialize()
+ {
+ return $this->tree;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/RecordService.php
+namespace Tqdev\PhpCrudApi\Record {
+
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Database\GenericDB;
+ use Tqdev\PhpCrudApi\Record\Document\ListDocument;
+
+ class RecordService
+ {
+ private $db;
+ private $reflection;
+ private $columns;
+ private $joiner;
+ private $filters;
+ private $ordering;
+ private $pagination;
+
+ public function __construct(GenericDB $db, ReflectionService $reflection)
+ {
+ $this->db = $db;
+ $this->reflection = $reflection;
+ $this->columns = new ColumnIncluder();
+ $this->joiner = new RelationJoiner($reflection, $this->columns);
+ $this->filters = new FilterInfo();
+ $this->ordering = new OrderingInfo();
+ $this->pagination = new PaginationInfo();
+ }
+
+ private function sanitizeRecord(string $tableName, /* object */ $record, string $id)
+ {
+ $keyset = array_keys((array) $record);
+ foreach ($keyset as $key) {
+ if (!$this->reflection->getTable($tableName)->hasColumn($key)) {
+ unset($record->$key);
+ }
+ }
+ if ($id != '') {
+ $pk = $this->reflection->getTable($tableName)->getPk();
+ foreach ($this->reflection->getTable($tableName)->getColumnNames() as $key) {
+ $field = $this->reflection->getTable($tableName)->getColumn($key);
+ if ($field->getName() == $pk->getName()) {
+ unset($record->$key);
+ }
+ }
+ }
+ }
+
+ public function hasTable(string $table): bool
+ {
+ return $this->reflection->hasTable($table);
+ }
+
+ public function getType(string $table): string
+ {
+ return $this->reflection->getType($table);
+ }
+
+ public function create(string $tableName, /* object */ $record, array $params) /*: ?int*/
+ {
+ $this->sanitizeRecord($tableName, $record, '');
+ $table = $this->reflection->getTable($tableName);
+ $columnValues = $this->columns->getValues($table, true, $record, $params);
+ return $this->db->createSingle($table, $columnValues);
+ }
+
+ public function read(string $tableName, string $id, array $params) /*: ?object*/
+ {
+ $table = $this->reflection->getTable($tableName);
+ $this->joiner->addMandatoryColumns($table, $params);
+ $columnNames = $this->columns->getNames($table, true, $params);
+ $record = $this->db->selectSingle($table, $columnNames, $id);
+ if ($record == null) {
+ return null;
+ }
+ $records = array($record);
+ $this->joiner->addJoins($table, $records, $params, $this->db);
+ return $records[0];
+ }
+
+ public function update(string $tableName, string $id, /* object */ $record, array $params) /*: ?int*/
+ {
+ $this->sanitizeRecord($tableName, $record, $id);
+ $table = $this->reflection->getTable($tableName);
+ $columnValues = $this->columns->getValues($table, true, $record, $params);
+ return $this->db->updateSingle($table, $columnValues, $id);
+ }
+
+ public function delete(string $tableName, string $id, array $params) /*: ?int*/
+ {
+ $table = $this->reflection->getTable($tableName);
+ return $this->db->deleteSingle($table, $id);
+ }
+
+ public function increment(string $tableName, string $id, /* object */ $record, array $params) /*: ?int*/
+ {
+ $this->sanitizeRecord($tableName, $record, $id);
+ $table = $this->reflection->getTable($tableName);
+ $columnValues = $this->columns->getValues($table, true, $record, $params);
+ return $this->db->incrementSingle($table, $columnValues, $id);
+ }
+
+ public function _list(string $tableName, array $params): ListDocument
+ {
+ $table = $this->reflection->getTable($tableName);
+ $this->joiner->addMandatoryColumns($table, $params);
+ $columnNames = $this->columns->getNames($table, true, $params);
+ $condition = $this->filters->getCombinedConditions($table, $params);
+ $columnOrdering = $this->ordering->getColumnOrdering($table, $params);
+ if (!$this->pagination->hasPage($params)) {
+ $offset = 0;
+ $limit = $this->pagination->getPageLimit($params);
+ $count = 0;
+ } else {
+ $offset = $this->pagination->getPageOffset($params);
+ $limit = $this->pagination->getPageLimit($params);
+ $count = $this->db->selectCount($table, $condition);
+ }
+ $records = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, $offset, $limit);
+ $this->joiner->addJoins($table, $records, $params, $this->db);
+ return new ListDocument($records, $count);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/RelationJoiner.php
+namespace Tqdev\PhpCrudApi\Record {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Database\GenericDB;
+ use Tqdev\PhpCrudApi\Middleware\Communication\VariableStore;
+ use Tqdev\PhpCrudApi\Record\Condition\ColumnCondition;
+ use Tqdev\PhpCrudApi\Record\Condition\OrCondition;
+
+ class RelationJoiner
+ {
+ private $reflection;
+ private $ordering;
+ private $columns;
+
+ public function __construct(ReflectionService $reflection, ColumnIncluder $columns)
+ {
+ $this->reflection = $reflection;
+ $this->ordering = new OrderingInfo();
+ $this->columns = $columns;
+ }
+
+ public function addMandatoryColumns(ReflectedTable $table, array &$params) /*: void*/
+ {
+ if (!isset($params['join']) || !isset($params['include'])) {
+ return;
+ }
+ $params['mandatory'] = array();
+ foreach ($params['join'] as $tableNames) {
+ $t1 = $table;
+ foreach (explode(',', $tableNames) as $tableName) {
+ if (!$this->reflection->hasTable($tableName)) {
+ continue;
+ }
+ $t2 = $this->reflection->getTable($tableName);
+ $fks1 = $t1->getFksTo($t2->getName());
+ $t3 = $this->hasAndBelongsToMany($t1, $t2);
+ if ($t3 != null || count($fks1) > 0) {
+ $params['mandatory'][] = $t2->getName() . '.' . $t2->getPk()->getName();
+ }
+ foreach ($fks1 as $fk) {
+ $params['mandatory'][] = $t1->getName() . '.' . $fk->getName();
+ }
+ $fks2 = $t2->getFksTo($t1->getName());
+ if ($t3 != null || count($fks2) > 0) {
+ $params['mandatory'][] = $t1->getName() . '.' . $t1->getPk()->getName();
+ }
+ foreach ($fks2 as $fk) {
+ $params['mandatory'][] = $t2->getName() . '.' . $fk->getName();
+ }
+ $t1 = $t2;
+ }
+ }
+ }
+
+ private function getJoinsAsPathTree(array $params): PathTree
+ {
+ $joins = new PathTree();
+ if (isset($params['join'])) {
+ foreach ($params['join'] as $tableNames) {
+ $path = array();
+ foreach (explode(',', $tableNames) as $tableName) {
+ if (!$this->reflection->hasTable($tableName)) {
+ continue;
+ }
+ $t = $this->reflection->getTable($tableName);
+ if ($t != null) {
+ $path[] = $t->getName();
+ }
+ }
+ $joins->put($path, true);
+ }
+ }
+ return $joins;
+ }
+
+ public function addJoins(ReflectedTable $table, array &$records, array $params, GenericDB $db) /*: void*/
+ {
+ $joins = $this->getJoinsAsPathTree($params);
+ $this->addJoinsForTables($table, $joins, $records, $params, $db);
+ }
+
+ private function hasAndBelongsToMany(ReflectedTable $t1, ReflectedTable $t2) /*: ?ReflectedTable*/
+ {
+ foreach ($this->reflection->getTableNames() as $tableName) {
+ $t3 = $this->reflection->getTable($tableName);
+ if (count($t3->getFksTo($t1->getName())) > 0 && count($t3->getFksTo($t2->getName())) > 0) {
+ return $t3;
+ }
+ }
+ return null;
+ }
+
+ private function addJoinsForTables(ReflectedTable $t1, PathTree $joins, array &$records, array $params, GenericDB $db)
+ {
+ foreach ($joins->getKeys() as $t2Name) {
+ $t2 = $this->reflection->getTable($t2Name);
+
+ $belongsTo = count($t1->getFksTo($t2->getName())) > 0;
+ $hasMany = count($t2->getFksTo($t1->getName())) > 0;
+ if (!$belongsTo && !$hasMany) {
+ $t3 = $this->hasAndBelongsToMany($t1, $t2);
+ } else {
+ $t3 = null;
+ }
+ $hasAndBelongsToMany = ($t3 != null);
+
+ $newRecords = array();
+ $fkValues = null;
+ $pkValues = null;
+ $habtmValues = null;
+
+ if ($belongsTo) {
+ $fkValues = $this->getFkEmptyValues($t1, $t2, $records);
+ $this->addFkRecords($t2, $fkValues, $params, $db, $newRecords);
+ }
+ if ($hasMany) {
+ $pkValues = $this->getPkEmptyValues($t1, $records);
+ $this->addPkRecords($t1, $t2, $pkValues, $params, $db, $newRecords);
+ }
+ if ($hasAndBelongsToMany) {
+ $habtmValues = $this->getHabtmEmptyValues($t1, $t2, $t3, $db, $records);
+ $this->addFkRecords($t2, $habtmValues->fkValues, $params, $db, $newRecords);
+ }
+
+ $this->addJoinsForTables($t2, $joins->get($t2Name), $newRecords, $params, $db);
+
+ if ($fkValues != null) {
+ $this->fillFkValues($t2, $newRecords, $fkValues);
+ $this->setFkValues($t1, $t2, $records, $fkValues);
+ }
+ if ($pkValues != null) {
+ $this->fillPkValues($t1, $t2, $newRecords, $pkValues);
+ $this->setPkValues($t1, $t2, $records, $pkValues);
+ }
+ if ($habtmValues != null) {
+ $this->fillFkValues($t2, $newRecords, $habtmValues->fkValues);
+ $this->setHabtmValues($t1, $t2, $records, $habtmValues);
+ }
+ }
+ }
+
+ private function getFkEmptyValues(ReflectedTable $t1, ReflectedTable $t2, array $records): array
+ {
+ $fkValues = array();
+ $fks = $t1->getFksTo($t2->getName());
+ foreach ($fks as $fk) {
+ $fkName = $fk->getName();
+ foreach ($records as $record) {
+ if (isset($record[$fkName])) {
+ $fkValue = $record[$fkName];
+ $fkValues[$fkValue] = null;
+ }
+ }
+ }
+ return $fkValues;
+ }
+
+ private function addFkRecords(ReflectedTable $t2, array $fkValues, array $params, GenericDB $db, array &$records) /*: void*/
+ {
+ $columnNames = $this->columns->getNames($t2, false, $params);
+ $fkIds = array_keys($fkValues);
+
+ foreach ($db->selectMultiple($t2, $columnNames, $fkIds) as $record) {
+ $records[] = $record;
+ }
+ }
+
+ private function fillFkValues(ReflectedTable $t2, array $fkRecords, array &$fkValues) /*: void*/
+ {
+ $pkName = $t2->getPk()->getName();
+ foreach ($fkRecords as $fkRecord) {
+ $pkValue = $fkRecord[$pkName];
+ $fkValues[$pkValue] = $fkRecord;
+ }
+ }
+
+ private function setFkValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, array $fkValues) /*: void*/
+ {
+ $fks = $t1->getFksTo($t2->getName());
+ foreach ($fks as $fk) {
+ $fkName = $fk->getName();
+ foreach ($records as $i => $record) {
+ if (isset($record[$fkName])) {
+ $key = $record[$fkName];
+ $records[$i][$fkName] = $fkValues[$key];
+ }
+ }
+ }
+ }
+
+ private function getPkEmptyValues(ReflectedTable $t1, array $records): array
+ {
+ $pkValues = array();
+ $pkName = $t1->getPk()->getName();
+ foreach ($records as $record) {
+ $key = $record[$pkName];
+ $pkValues[$key] = array();
+ }
+ return $pkValues;
+ }
+
+ private function addPkRecords(ReflectedTable $t1, ReflectedTable $t2, array $pkValues, array $params, GenericDB $db, array &$records) /*: void*/
+ {
+ $fks = $t2->getFksTo($t1->getName());
+ $columnNames = $this->columns->getNames($t2, false, $params);
+ $pkValueKeys = implode(',', array_keys($pkValues));
+ $conditions = array();
+ foreach ($fks as $fk) {
+ $conditions[] = new ColumnCondition($fk, 'in', $pkValueKeys);
+ }
+ $condition = OrCondition::fromArray($conditions);
+ $columnOrdering = array();
+ $limit = VariableStore::get("joinLimits.maxRecords") ?: -1;
+ if ($limit != -1) {
+ $columnOrdering = $this->ordering->getDefaultColumnOrdering($t2);
+ }
+ foreach ($db->selectAll($t2, $columnNames, $condition, $columnOrdering, 0, $limit) as $record) {
+ $records[] = $record;
+ }
+ }
+
+ private function fillPkValues(ReflectedTable $t1, ReflectedTable $t2, array $pkRecords, array &$pkValues) /*: void*/
+ {
+ $fks = $t2->getFksTo($t1->getName());
+ foreach ($fks as $fk) {
+ $fkName = $fk->getName();
+ foreach ($pkRecords as $pkRecord) {
+ $key = $pkRecord[$fkName];
+ if (isset($pkValues[$key])) {
+ $pkValues[$key][] = $pkRecord;
+ }
+ }
+ }
+ }
+
+ private function setPkValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, array $pkValues) /*: void*/
+ {
+ $pkName = $t1->getPk()->getName();
+ $t2Name = $t2->getName();
+
+ foreach ($records as $i => $record) {
+ $key = $record[$pkName];
+ $records[$i][$t2Name] = $pkValues[$key];
+ }
+ }
+
+ private function getHabtmEmptyValues(ReflectedTable $t1, ReflectedTable $t2, ReflectedTable $t3, GenericDB $db, array $records): HabtmValues
+ {
+ $pkValues = $this->getPkEmptyValues($t1, $records);
+ $fkValues = array();
+
+ $fk1 = $t3->getFksTo($t1->getName())[0];
+ $fk2 = $t3->getFksTo($t2->getName())[0];
+
+ $fk1Name = $fk1->getName();
+ $fk2Name = $fk2->getName();
+
+ $columnNames = array($fk1Name, $fk2Name);
+
+ $pkIds = implode(',', array_keys($pkValues));
+ $condition = new ColumnCondition($t3->getColumn($fk1Name), 'in', $pkIds);
+ $columnOrdering = array();
+
+ $limit = VariableStore::get("joinLimits.maxRecords") ?: -1;
+ if ($limit != -1) {
+ $columnOrdering = $this->ordering->getDefaultColumnOrdering($t3);
+ }
+ $records = $db->selectAll($t3, $columnNames, $condition, $columnOrdering, 0, $limit);
+ foreach ($records as $record) {
+ $val1 = $record[$fk1Name];
+ $val2 = $record[$fk2Name];
+ $pkValues[$val1][] = $val2;
+ $fkValues[$val2] = null;
+ }
+
+ return new HabtmValues($pkValues, $fkValues);
+ }
+
+ private function setHabtmValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, HabtmValues $habtmValues) /*: void*/
+ {
+ $pkName = $t1->getPk()->getName();
+ $t2Name = $t2->getName();
+ foreach ($records as $i => $record) {
+ $key = $record[$pkName];
+ $val = array();
+ $fks = $habtmValues->pkValues[$key];
+ foreach ($fks as $fk) {
+ $val[] = $habtmValues->fkValues[$fk];
+ }
+ $records[$i][$t2Name] = $val;
+ }
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Api.php
+namespace Tqdev\PhpCrudApi {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Cache\CacheFactory;
+ use Tqdev\PhpCrudApi\Column\DefinitionService;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Controller\CacheController;
+ use Tqdev\PhpCrudApi\Controller\ColumnController;
+ use Tqdev\PhpCrudApi\Controller\GeoJsonController;
+ use Tqdev\PhpCrudApi\Controller\JsonResponder;
+ use Tqdev\PhpCrudApi\Controller\OpenApiController;
+ use Tqdev\PhpCrudApi\Controller\RecordController;
+ use Tqdev\PhpCrudApi\Database\GenericDB;
+ use Tqdev\PhpCrudApi\GeoJson\GeoJsonService;
+ use Tqdev\PhpCrudApi\Middleware\AuthorizationMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\BasicAuthMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\CorsMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\CustomizationMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\DbAuthMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\FirewallMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\IpAddressMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\JoinLimitsMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\JwtAuthMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\MultiTenancyMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\PageLimitsMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\ReconnectMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\Router\SimpleRouter;
+ use Tqdev\PhpCrudApi\Middleware\SanitationMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\SslRedirectMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\ValidationMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\XmlMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\XsrfMiddleware;
+ use Tqdev\PhpCrudApi\OpenApi\OpenApiService;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\Record\RecordService;
+ use Tqdev\PhpCrudApi\ResponseUtils;
+
+ class Api implements RequestHandlerInterface
+ {
+ private $router;
+ private $responder;
+ private $debug;
+
+ public function __construct(Config $config)
+ {
+ $db = new GenericDB(
+ $config->getDriver(),
+ $config->getAddress(),
+ $config->getPort(),
+ $config->getDatabase(),
+ $config->getTables(),
+ $config->getUsername(),
+ $config->getPassword()
+ );
+ $prefix = sprintf('phpcrudapi-%s-', substr(md5(__FILE__), 0, 8));
+ $cache = CacheFactory::create($config->getCacheType(), $prefix, $config->getCachePath());
+ $reflection = new ReflectionService($db, $cache, $config->getCacheTime());
+ $responder = new JsonResponder();
+ $router = new SimpleRouter($config->getBasePath(), $responder, $cache, $config->getCacheTime(), $config->getDebug());
+ foreach ($config->getMiddlewares() as $middleware => $properties) {
+ switch ($middleware) {
+ case 'sslRedirect':
+ new SslRedirectMiddleware($router, $responder, $properties);
+ break;
+ case 'cors':
+ new CorsMiddleware($router, $responder, $properties, $config->getDebug());
+ break;
+ case 'firewall':
+ new FirewallMiddleware($router, $responder, $properties);
+ break;
+ case 'basicAuth':
+ new BasicAuthMiddleware($router, $responder, $properties);
+ break;
+ case 'jwtAuth':
+ new JwtAuthMiddleware($router, $responder, $properties);
+ break;
+ case 'dbAuth':
+ new DbAuthMiddleware($router, $responder, $properties, $reflection, $db);
+ break;
+ case 'reconnect':
+ new ReconnectMiddleware($router, $responder, $properties, $reflection, $db);
+ break;
+ case 'validation':
+ new ValidationMiddleware($router, $responder, $properties, $reflection);
+ break;
+ case 'ipAddress':
+ new IpAddressMiddleware($router, $responder, $properties, $reflection);
+ break;
+ case 'sanitation':
+ new SanitationMiddleware($router, $responder, $properties, $reflection);
+ break;
+ case 'multiTenancy':
+ new MultiTenancyMiddleware($router, $responder, $properties, $reflection);
+ break;
+ case 'authorization':
+ new AuthorizationMiddleware($router, $responder, $properties, $reflection);
+ break;
+ case 'xsrf':
+ new XsrfMiddleware($router, $responder, $properties);
+ break;
+ case 'pageLimits':
+ new PageLimitsMiddleware($router, $responder, $properties, $reflection);
+ break;
+ case 'joinLimits':
+ new JoinLimitsMiddleware($router, $responder, $properties, $reflection);
+ break;
+ case 'customization':
+ new CustomizationMiddleware($router, $responder, $properties, $reflection);
+ break;
+ case 'xml':
+ new XmlMiddleware($router, $responder, $properties, $reflection);
+ break;
+ }
+ }
+ foreach ($config->getControllers() as $controller) {
+ switch ($controller) {
+ case 'records':
+ $records = new RecordService($db, $reflection);
+ new RecordController($router, $responder, $records);
+ break;
+ case 'columns':
+ $definition = new DefinitionService($db, $reflection);
+ new ColumnController($router, $responder, $reflection, $definition);
+ break;
+ case 'cache':
+ new CacheController($router, $responder, $cache);
+ break;
+ case 'openapi':
+ $openApi = new OpenApiService($reflection, $config->getOpenApiBase(), $config->getControllers(), $config->getCustomOpenApiBuilders());
+ new OpenApiController($router, $responder, $openApi);
+ break;
+ case 'geojson':
+ $records = new RecordService($db, $reflection);
+ $geoJson = new GeoJsonService($reflection, $records);
+ new GeoJsonController($router, $responder, $geoJson);
+ break;
+ }
+ }
+ foreach ($config->getCustomControllers() as $className) {
+ if (class_exists($className)) {
+ $records = new RecordService($db, $reflection);
+ new $className($router, $responder, $records);
+ }
+ }
+ $this->router = $router;
+ $this->responder = $responder;
+ $this->debug = $config->getDebug();
+ }
+
+ private function parseBody(string $body) /*: ?object*/
+ {
+ $first = substr($body, 0, 1);
+ if ($first == '[' || $first == '{') {
+ $object = json_decode($body);
+ $causeCode = json_last_error();
+ if ($causeCode !== JSON_ERROR_NONE) {
+ $object = null;
+ }
+ } else {
+ parse_str($body, $input);
+ foreach ($input as $key => $value) {
+ if (substr($key, -9) == '__is_null') {
+ $input[substr($key, 0, -9)] = null;
+ unset($input[$key]);
+ }
+ }
+ $object = (object) $input;
+ }
+ return $object;
+ }
+
+ private function addParsedBody(ServerRequestInterface $request): ServerRequestInterface
+ {
+ $parsedBody = $request->getParsedBody();
+ if ($parsedBody) {
+ $request = $this->applyParsedBodyHack($request);
+ } else {
+ $body = $request->getBody();
+ if ($body->isReadable()) {
+ if ($body->isSeekable()) {
+ $body->rewind();
+ }
+ $contents = $body->getContents();
+ if ($body->isSeekable()) {
+ $body->rewind();
+ }
+ if ($contents) {
+ $parsedBody = $this->parseBody($contents);
+ $request = $request->withParsedBody($parsedBody);
+ }
+ }
+ }
+ return $request;
+ }
+
+ private function applyParsedBodyHack(ServerRequestInterface $request): ServerRequestInterface
+ {
+ $parsedBody = $request->getParsedBody();
+ if (is_array($parsedBody)) { // is it really?
+ $contents = json_encode($parsedBody);
+ $parsedBody = $this->parseBody($contents);
+ $request = $request->withParsedBody($parsedBody);
+ }
+ return $request;
+ }
+
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ return $this->router->route($this->addParsedBody($request));
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Config.php
+namespace Tqdev\PhpCrudApi {
+
+ class Config
+ {
+ private $values = [
+ 'driver' => null,
+ 'address' => 'localhost',
+ 'port' => null,
+ 'username' => null,
+ 'password' => null,
+ 'database' => null,
+ 'tables' => '',
+ 'middlewares' => 'cors,errors',
+ 'controllers' => 'records,geojson,openapi',
+ 'customControllers' => '',
+ 'customOpenApiBuilders' => '',
+ 'cacheType' => 'TempFile',
+ 'cachePath' => '',
+ 'cacheTime' => 10,
+ 'debug' => false,
+ 'basePath' => '',
+ 'openApiBase' => '{"info":{"title":"PHP-CRUD-API","version":"1.0.0"}}',
+ ];
+
+ private function getDefaultDriver(array $values): string
+ {
+ if (isset($values['driver'])) {
+ return $values['driver'];
+ }
+ return 'mysql';
+ }
+
+ private function getDefaultPort(string $driver): int
+ {
+ switch ($driver) {
+ case 'mysql':
+ return 3306;
+ case 'pgsql':
+ return 5432;
+ case 'sqlsrv':
+ return 1433;
+ case 'sqlite':
+ return 0;
+ }
+ }
+
+ private function getDefaultAddress(string $driver): string
+ {
+ switch ($driver) {
+ case 'mysql':
+ return 'localhost';
+ case 'pgsql':
+ return 'localhost';
+ case 'sqlsrv':
+ return 'localhost';
+ case 'sqlite':
+ return 'data.db';
+ }
+ }
+
+ private function getDriverDefaults(string $driver): array
+ {
+ return [
+ 'driver' => $driver,
+ 'address' => $this->getDefaultAddress($driver),
+ 'port' => $this->getDefaultPort($driver),
+ ];
+ }
+
+ private function applyEnvironmentVariables(array $values): array
+ {
+ $newValues = array();
+ foreach ($values as $key => $value) {
+ $environmentKey = 'PHP_CRUD_API_' . strtoupper(preg_replace('/(?getDefaultDriver($values);
+ $defaults = $this->getDriverDefaults($driver);
+ $newValues = array_merge($this->values, $defaults, $values);
+ $newValues = $this->parseMiddlewares($newValues);
+ $diff = array_diff_key($newValues, $this->values);
+ if (!empty($diff)) {
+ $key = array_keys($diff)[0];
+ throw new \Exception("Config has invalid value '$key'");
+ }
+ $newValues = $this->applyEnvironmentVariables($newValues);
+ $this->values = $newValues;
+ }
+
+ private function parseMiddlewares(array $values): array
+ {
+ $newValues = array();
+ $properties = array();
+ $middlewares = array_map('trim', explode(',', $values['middlewares']));
+ foreach ($middlewares as $middleware) {
+ $properties[$middleware] = [];
+ }
+ foreach ($values as $key => $value) {
+ if (strpos($key, '.') === false) {
+ $newValues[$key] = $value;
+ } else {
+ list($middleware, $key2) = explode('.', $key, 2);
+ if (isset($properties[$middleware])) {
+ $properties[$middleware][$key2] = $value;
+ } else {
+ throw new \Exception("Config has invalid value '$key'");
+ }
+ }
+ }
+ $newValues['middlewares'] = array_reverse($properties, true);
+ return $newValues;
+ }
+
+ public function getDriver(): string
+ {
+ return $this->values['driver'];
+ }
+
+ public function getAddress(): string
+ {
+ return $this->values['address'];
+ }
+
+ public function getPort(): int
+ {
+ return $this->values['port'];
+ }
+
+ public function getUsername(): string
+ {
+ return $this->values['username'];
+ }
+
+ public function getPassword(): string
+ {
+ return $this->values['password'];
+ }
+
+ public function getDatabase(): string
+ {
+ return $this->values['database'];
+ }
+
+ public function getTables(): array
+ {
+ return array_filter(array_map('trim', explode(',', $this->values['tables'])));
+ }
+
+ public function getMiddlewares(): array
+ {
+ return $this->values['middlewares'];
+ }
+
+ public function getControllers(): array
+ {
+ return array_filter(array_map('trim', explode(',', $this->values['controllers'])));
+ }
+
+ public function getCustomControllers(): array
+ {
+ return array_filter(array_map('trim', explode(',', $this->values['customControllers'])));
+ }
+
+ public function getCustomOpenApiBuilders(): array
+ {
+ return array_filter(array_map('trim', explode(',', $this->values['customOpenApiBuilders'])));
+ }
+
+ public function getCacheType(): string
+ {
+ return $this->values['cacheType'];
+ }
+
+ public function getCachePath(): string
+ {
+ return $this->values['cachePath'];
+ }
+
+ public function getCacheTime(): int
+ {
+ return $this->values['cacheTime'];
+ }
+
+ public function getDebug(): bool
+ {
+ return $this->values['debug'];
+ }
+
+ public function getBasePath(): string
+ {
+ return $this->values['basePath'];
+ }
+
+ public function getOpenApiBase(): array
+ {
+ return json_decode($this->values['openApiBase'], true);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/RequestFactory.php
+namespace Tqdev\PhpCrudApi {
+
+ use Nyholm\Psr7\Factory\Psr17Factory;
+ use Nyholm\Psr7Server\ServerRequestCreator;
+ use Psr\Http\Message\ServerRequestInterface;
+
+ class RequestFactory
+ {
+ public static function fromGlobals(): ServerRequestInterface
+ {
+ $psr17Factory = new Psr17Factory();
+ $creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
+ $serverRequest = $creator->fromGlobals();
+ $stream = $psr17Factory->createStreamFromFile('php://input');
+ $serverRequest = $serverRequest->withBody($stream);
+ return $serverRequest;
+ }
+
+ public static function fromString(string $request): ServerRequestInterface
+ {
+ $parts = explode("\n\n", trim($request), 2);
+ $lines = explode("\n", $parts[0]);
+ $first = explode(' ', trim(array_shift($lines)), 2);
+ $method = $first[0];
+ $body = isset($parts[1]) ? $parts[1] : '';
+ $url = isset($first[1]) ? $first[1] : '';
+
+ $psr17Factory = new Psr17Factory();
+ $serverRequest = $psr17Factory->createServerRequest($method, $url);
+ foreach ($lines as $line) {
+ list($key, $value) = explode(':', $line, 2);
+ $serverRequest = $serverRequest->withAddedHeader($key, $value);
+ }
+ if ($body) {
+ $stream = $psr17Factory->createStream($body);
+ $stream->rewind();
+ $serverRequest = $serverRequest->withBody($stream);
+ }
+ return $serverRequest;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/RequestUtils.php
+namespace Tqdev\PhpCrudApi {
+
+ use Psr\Http\Message\ServerRequestInterface;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+
+ class RequestUtils
+ {
+ public static function setParams(ServerRequestInterface $request, array $params): ServerRequestInterface
+ {
+ $query = preg_replace('|%5B[0-9]+%5D=|', '=', http_build_query($params));
+ return $request->withUri($request->getUri()->withQuery($query));
+ }
+
+ public static function getHeader(ServerRequestInterface $request, string $header): string
+ {
+ $headers = $request->getHeader($header);
+ return isset($headers[0]) ? $headers[0] : '';
+ }
+
+ public static function getParams(ServerRequestInterface $request): array
+ {
+ $params = array();
+ $query = $request->getUri()->getQuery();
+ //$query = str_replace('][]=', ']=', str_replace('=', '[]=', $query));
+ $query = str_replace('%5D%5B%5D=', '%5D=', str_replace('=', '%5B%5D=', $query));
+ parse_str($query, $params);
+ return $params;
+ }
+
+ public static function getPathSegment(ServerRequestInterface $request, int $part): string
+ {
+ $path = $request->getUri()->getPath();
+ $pathSegments = explode('/', rtrim($path, '/'));
+ if ($part < 0 || $part >= count($pathSegments)) {
+ return '';
+ }
+ return urldecode($pathSegments[$part]);
+ }
+
+ public static function getOperation(ServerRequestInterface $request): string
+ {
+ $method = $request->getMethod();
+ $path = RequestUtils::getPathSegment($request, 1);
+ $hasPk = RequestUtils::getPathSegment($request, 3) != '';
+ switch ($path) {
+ case 'openapi':
+ return 'document';
+ case 'columns':
+ return $method == 'get' ? 'reflect' : 'remodel';
+ case 'geojson':
+ case 'records':
+ switch ($method) {
+ case 'POST':
+ return 'create';
+ case 'GET':
+ return $hasPk ? 'read' : 'list';
+ case 'PUT':
+ return 'update';
+ case 'DELETE':
+ return 'delete';
+ case 'PATCH':
+ return 'increment';
+ }
+ }
+ return 'unknown';
+ }
+
+ private static function getJoinTables(string $tableName, array $parameters): array
+ {
+ $uniqueTableNames = array();
+ $uniqueTableNames[$tableName] = true;
+ if (isset($parameters['join'])) {
+ foreach ($parameters['join'] as $parameter) {
+ $tableNames = explode(',', trim($parameter));
+ foreach ($tableNames as $tableName) {
+ $uniqueTableNames[$tableName] = true;
+ }
+ }
+ }
+ return array_keys($uniqueTableNames);
+ }
+
+ public static function getTableNames(ServerRequestInterface $request, ReflectionService $reflection): array
+ {
+ $path = RequestUtils::getPathSegment($request, 1);
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ $allTableNames = $reflection->getTableNames();
+ switch ($path) {
+ case 'openapi':
+ return $allTableNames;
+ case 'columns':
+ return $tableName ? [$tableName] : $allTableNames;
+ case 'records':
+ return self::getJoinTables($tableName, RequestUtils::getParams($request));
+ }
+ return $allTableNames;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/ResponseFactory.php
+namespace Tqdev\PhpCrudApi {
+
+ use Nyholm\Psr7\Factory\Psr17Factory;
+ use Psr\Http\Message\ResponseInterface;
+
+ class ResponseFactory
+ {
+ const OK = 200;
+ const MOVED_PERMANENTLY = 301;
+ const FOUND = 302;
+ const UNAUTHORIZED = 401;
+ const FORBIDDEN = 403;
+ const NOT_FOUND = 404;
+ const METHOD_NOT_ALLOWED = 405;
+ const CONFLICT = 409;
+ const UNPROCESSABLE_ENTITY = 422;
+ const INTERNAL_SERVER_ERROR = 500;
+
+ public static function fromXml(int $status, string $xml): ResponseInterface
+ {
+ return self::from($status, 'text/xml', $xml);
+ }
+
+ public static function fromCsv(int $status, string $csv): ResponseInterface
+ {
+ return self::from($status, 'text/csv', $csv);
+ }
+
+ public static function fromHtml(int $status, string $html): ResponseInterface
+ {
+ return self::from($status, 'text/html', $html);
+ }
+
+ public static function fromObject(int $status, $body): ResponseInterface
+ {
+ $content = json_encode($body, JSON_UNESCAPED_UNICODE);
+ return self::from($status, 'application/json', $content);
+ }
+
+ public static function from(int $status, string $contentType, string $content): ResponseInterface
+ {
+ $psr17Factory = new Psr17Factory();
+ $response = $psr17Factory->createResponse($status);
+ $stream = $psr17Factory->createStream($content);
+ $stream->rewind();
+ $response = $response->withBody($stream);
+ $response = $response->withHeader('Content-Type', $contentType . '; charset=utf-8');
+ $response = $response->withHeader('Content-Length', strlen($content));
+ return $response;
+ }
+
+ public static function fromStatus(int $status): ResponseInterface
+ {
+ $psr17Factory = new Psr17Factory();
+ return $psr17Factory->createResponse($status);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/ResponseUtils.php
+namespace Tqdev\PhpCrudApi {
+
+ use Psr\Http\Message\ResponseInterface;
+
+ class ResponseUtils
+ {
+ public static function output(ResponseInterface $response)
+ {
+ $status = $response->getStatusCode();
+ $headers = $response->getHeaders();
+ $body = $response->getBody()->getContents();
+
+ http_response_code($status);
+ foreach ($headers as $key => $values) {
+ foreach ($values as $value) {
+ header("$key: $value");
+ }
+ }
+ echo $body;
+ }
+
+ public static function addExceptionHeaders(ResponseInterface $response, \Throwable $e): ResponseInterface
+ {
+ $response = $response->withHeader('X-Exception-Name', get_class($e));
+ $response = $response->withHeader('X-Exception-Message', preg_replace('|\n|', ' ', trim($e->getMessage())));
+ $response = $response->withHeader('X-Exception-File', $e->getFile() . ':' . $e->getLine());
+ return $response;
+ }
+
+ public static function toString(ResponseInterface $response): string
+ {
+ $status = $response->getStatusCode();
+ $headers = $response->getHeaders();
+ $body = $response->getBody()->getContents();
+
+ $str = "$status\n";
+ foreach ($headers as $key => $values) {
+ foreach ($values as $value) {
+ $str .= "$key: $value\n";
+ }
+ }
+ if ($body !== '') {
+ $str .= "\n";
+ $str .= "$body\n";
+ }
+ return $str;
+ }
+ }
+}
diff --git a/seatmap-webapi/api.php b/seatmap-webapi/api.php
new file mode 100644
index 0000000..9a7512f
--- /dev/null
+++ b/seatmap-webapi/api.php
@@ -0,0 +1,11410 @@
+getHeaders() as $name => $values) {
+ * echo $name . ": " . implode(", ", $values);
+ * }
+ *
+ * // Emit headers iteratively:
+ * foreach ($message->getHeaders() as $name => $values) {
+ * foreach ($values as $value) {
+ * header(sprintf('%s: %s', $name, $value), false);
+ * }
+ * }
+ *
+ * While header names are not case-sensitive, getHeaders() will preserve the
+ * exact case in which headers were originally specified.
+ *
+ * @return string[][] Returns an associative array of the message's headers. Each
+ * key MUST be a header name, and each value MUST be an array of strings
+ * for that header.
+ */
+ public function getHeaders();
+
+ /**
+ * Checks if a header exists by the given case-insensitive name.
+ *
+ * @param string $name Case-insensitive header field name.
+ * @return bool Returns true if any header names match the given header
+ * name using a case-insensitive string comparison. Returns false if
+ * no matching header name is found in the message.
+ */
+ public function hasHeader($name);
+
+ /**
+ * Retrieves a message header value by the given case-insensitive name.
+ *
+ * This method returns an array of all the header values of the given
+ * case-insensitive header name.
+ *
+ * If the header does not appear in the message, this method MUST return an
+ * empty array.
+ *
+ * @param string $name Case-insensitive header field name.
+ * @return string[] An array of string values as provided for the given
+ * header. If the header does not appear in the message, this method MUST
+ * return an empty array.
+ */
+ public function getHeader($name);
+
+ /**
+ * Retrieves a comma-separated string of the values for a single header.
+ *
+ * This method returns all of the header values of the given
+ * case-insensitive header name as a string concatenated together using
+ * a comma.
+ *
+ * NOTE: Not all header values may be appropriately represented using
+ * comma concatenation. For such headers, use getHeader() instead
+ * and supply your own delimiter when concatenating.
+ *
+ * If the header does not appear in the message, this method MUST return
+ * an empty string.
+ *
+ * @param string $name Case-insensitive header field name.
+ * @return string A string of values as provided for the given header
+ * concatenated together using a comma. If the header does not appear in
+ * the message, this method MUST return an empty string.
+ */
+ public function getHeaderLine($name);
+
+ /**
+ * Return an instance with the provided value replacing the specified header.
+ *
+ * While header names are case-insensitive, the casing of the header will
+ * be preserved by this function, and returned from getHeaders().
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * new and/or updated header and value.
+ *
+ * @param string $name Case-insensitive header field name.
+ * @param string|string[] $value Header value(s).
+ * @return static
+ * @throws \InvalidArgumentException for invalid header names or values.
+ */
+ public function withHeader($name, $value);
+
+ /**
+ * Return an instance with the specified header appended with the given value.
+ *
+ * Existing values for the specified header will be maintained. The new
+ * value(s) will be appended to the existing list. If the header did not
+ * exist previously, it will be added.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * new header and/or value.
+ *
+ * @param string $name Case-insensitive header field name to add.
+ * @param string|string[] $value Header value(s).
+ * @return static
+ * @throws \InvalidArgumentException for invalid header names or values.
+ */
+ public function withAddedHeader($name, $value);
+
+ /**
+ * Return an instance without the specified header.
+ *
+ * Header resolution MUST be done without case-sensitivity.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that removes
+ * the named header.
+ *
+ * @param string $name Case-insensitive header field name to remove.
+ * @return static
+ */
+ public function withoutHeader($name);
+
+ /**
+ * Gets the body of the message.
+ *
+ * @return StreamInterface Returns the body as a stream.
+ */
+ public function getBody();
+
+ /**
+ * Return an instance with the specified message body.
+ *
+ * The body MUST be a StreamInterface object.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return a new instance that has the
+ * new body stream.
+ *
+ * @param StreamInterface $body Body.
+ * @return static
+ * @throws \InvalidArgumentException When the body is not valid.
+ */
+ public function withBody(StreamInterface $body);
+ }
+}
+
+// file: vendor/psr/http-message/src/RequestInterface.php
+namespace Psr\Http\Message {
+
+ /**
+ * Representation of an outgoing, client-side request.
+ *
+ * Per the HTTP specification, this interface includes properties for
+ * each of the following:
+ *
+ * - Protocol version
+ * - HTTP method
+ * - URI
+ * - Headers
+ * - Message body
+ *
+ * During construction, implementations MUST attempt to set the Host header from
+ * a provided URI if no Host header is provided.
+ *
+ * Requests are considered immutable; all methods that might change state MUST
+ * be implemented such that they retain the internal state of the current
+ * message and return an instance that contains the changed state.
+ */
+ interface RequestInterface extends MessageInterface
+ {
+ /**
+ * Retrieves the message's request target.
+ *
+ * Retrieves the message's request-target either as it will appear (for
+ * clients), as it appeared at request (for servers), or as it was
+ * specified for the instance (see withRequestTarget()).
+ *
+ * In most cases, this will be the origin-form of the composed URI,
+ * unless a value was provided to the concrete implementation (see
+ * withRequestTarget() below).
+ *
+ * If no URI is available, and no request-target has been specifically
+ * provided, this method MUST return the string "/".
+ *
+ * @return string
+ */
+ public function getRequestTarget();
+
+ /**
+ * Return an instance with the specific request-target.
+ *
+ * If the request needs a non-origin-form request-target — e.g., for
+ * specifying an absolute-form, authority-form, or asterisk-form —
+ * this method may be used to create an instance with the specified
+ * request-target, verbatim.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * changed request target.
+ *
+ * @link http://tools.ietf.org/html/rfc7230#section-5.3 (for the various
+ * request-target forms allowed in request messages)
+ * @param mixed $requestTarget
+ * @return static
+ */
+ public function withRequestTarget($requestTarget);
+
+ /**
+ * Retrieves the HTTP method of the request.
+ *
+ * @return string Returns the request method.
+ */
+ public function getMethod();
+
+ /**
+ * Return an instance with the provided HTTP method.
+ *
+ * While HTTP method names are typically all uppercase characters, HTTP
+ * method names are case-sensitive and thus implementations SHOULD NOT
+ * modify the given string.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * changed request method.
+ *
+ * @param string $method Case-sensitive method.
+ * @return static
+ * @throws \InvalidArgumentException for invalid HTTP methods.
+ */
+ public function withMethod($method);
+
+ /**
+ * Retrieves the URI instance.
+ *
+ * This method MUST return a UriInterface instance.
+ *
+ * @link http://tools.ietf.org/html/rfc3986#section-4.3
+ * @return UriInterface Returns a UriInterface instance
+ * representing the URI of the request.
+ */
+ public function getUri();
+
+ /**
+ * Returns an instance with the provided URI.
+ *
+ * This method MUST update the Host header of the returned request by
+ * default if the URI contains a host component. If the URI does not
+ * contain a host component, any pre-existing Host header MUST be carried
+ * over to the returned request.
+ *
+ * You can opt-in to preserving the original state of the Host header by
+ * setting `$preserveHost` to `true`. When `$preserveHost` is set to
+ * `true`, this method interacts with the Host header in the following ways:
+ *
+ * - If the Host header is missing or empty, and the new URI contains
+ * a host component, this method MUST update the Host header in the returned
+ * request.
+ * - If the Host header is missing or empty, and the new URI does not contain a
+ * host component, this method MUST NOT update the Host header in the returned
+ * request.
+ * - If a Host header is present and non-empty, this method MUST NOT update
+ * the Host header in the returned request.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * new UriInterface instance.
+ *
+ * @link http://tools.ietf.org/html/rfc3986#section-4.3
+ * @param UriInterface $uri New request URI to use.
+ * @param bool $preserveHost Preserve the original state of the Host header.
+ * @return static
+ */
+ public function withUri(UriInterface $uri, $preserveHost = false);
+ }
+}
+
+// file: vendor/psr/http-message/src/ResponseInterface.php
+namespace Psr\Http\Message {
+
+ /**
+ * Representation of an outgoing, server-side response.
+ *
+ * Per the HTTP specification, this interface includes properties for
+ * each of the following:
+ *
+ * - Protocol version
+ * - Status code and reason phrase
+ * - Headers
+ * - Message body
+ *
+ * Responses are considered immutable; all methods that might change state MUST
+ * be implemented such that they retain the internal state of the current
+ * message and return an instance that contains the changed state.
+ */
+ interface ResponseInterface extends MessageInterface
+ {
+ /**
+ * Gets the response status code.
+ *
+ * The status code is a 3-digit integer result code of the server's attempt
+ * to understand and satisfy the request.
+ *
+ * @return int Status code.
+ */
+ public function getStatusCode();
+
+ /**
+ * Return an instance with the specified status code and, optionally, reason phrase.
+ *
+ * If no reason phrase is specified, implementations MAY choose to default
+ * to the RFC 7231 or IANA recommended reason phrase for the response's
+ * status code.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated status and reason phrase.
+ *
+ * @link http://tools.ietf.org/html/rfc7231#section-6
+ * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+ * @param int $code The 3-digit integer result code to set.
+ * @param string $reasonPhrase The reason phrase to use with the
+ * provided status code; if none is provided, implementations MAY
+ * use the defaults as suggested in the HTTP specification.
+ * @return static
+ * @throws \InvalidArgumentException For invalid status code arguments.
+ */
+ public function withStatus($code, $reasonPhrase = '');
+
+ /**
+ * Gets the response reason phrase associated with the status code.
+ *
+ * Because a reason phrase is not a required element in a response
+ * status line, the reason phrase value MAY be null. Implementations MAY
+ * choose to return the default RFC 7231 recommended reason phrase (or those
+ * listed in the IANA HTTP Status Code Registry) for the response's
+ * status code.
+ *
+ * @link http://tools.ietf.org/html/rfc7231#section-6
+ * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+ * @return string Reason phrase; must return an empty string if none present.
+ */
+ public function getReasonPhrase();
+ }
+}
+
+// file: vendor/psr/http-message/src/ServerRequestInterface.php
+namespace Psr\Http\Message {
+
+ /**
+ * Representation of an incoming, server-side HTTP request.
+ *
+ * Per the HTTP specification, this interface includes properties for
+ * each of the following:
+ *
+ * - Protocol version
+ * - HTTP method
+ * - URI
+ * - Headers
+ * - Message body
+ *
+ * Additionally, it encapsulates all data as it has arrived to the
+ * application from the CGI and/or PHP environment, including:
+ *
+ * - The values represented in $_SERVER.
+ * - Any cookies provided (generally via $_COOKIE)
+ * - Query string arguments (generally via $_GET, or as parsed via parse_str())
+ * - Upload files, if any (as represented by $_FILES)
+ * - Deserialized body parameters (generally from $_POST)
+ *
+ * $_SERVER values MUST be treated as immutable, as they represent application
+ * state at the time of request; as such, no methods are provided to allow
+ * modification of those values. The other values provide such methods, as they
+ * can be restored from $_SERVER or the request body, and may need treatment
+ * during the application (e.g., body parameters may be deserialized based on
+ * content type).
+ *
+ * Additionally, this interface recognizes the utility of introspecting a
+ * request to derive and match additional parameters (e.g., via URI path
+ * matching, decrypting cookie values, deserializing non-form-encoded body
+ * content, matching authorization headers to users, etc). These parameters
+ * are stored in an "attributes" property.
+ *
+ * Requests are considered immutable; all methods that might change state MUST
+ * be implemented such that they retain the internal state of the current
+ * message and return an instance that contains the changed state.
+ */
+ interface ServerRequestInterface extends RequestInterface
+ {
+ /**
+ * Retrieve server parameters.
+ *
+ * Retrieves data related to the incoming request environment,
+ * typically derived from PHP's $_SERVER superglobal. The data IS NOT
+ * REQUIRED to originate from $_SERVER.
+ *
+ * @return array
+ */
+ public function getServerParams();
+
+ /**
+ * Retrieve cookies.
+ *
+ * Retrieves cookies sent by the client to the server.
+ *
+ * The data MUST be compatible with the structure of the $_COOKIE
+ * superglobal.
+ *
+ * @return array
+ */
+ public function getCookieParams();
+
+ /**
+ * Return an instance with the specified cookies.
+ *
+ * The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST
+ * be compatible with the structure of $_COOKIE. Typically, this data will
+ * be injected at instantiation.
+ *
+ * This method MUST NOT update the related Cookie header of the request
+ * instance, nor related values in the server params.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated cookie values.
+ *
+ * @param array $cookies Array of key/value pairs representing cookies.
+ * @return static
+ */
+ public function withCookieParams(array $cookies);
+
+ /**
+ * Retrieve query string arguments.
+ *
+ * Retrieves the deserialized query string arguments, if any.
+ *
+ * Note: the query params might not be in sync with the URI or server
+ * params. If you need to ensure you are only getting the original
+ * values, you may need to parse the query string from `getUri()->getQuery()`
+ * or from the `QUERY_STRING` server param.
+ *
+ * @return array
+ */
+ public function getQueryParams();
+
+ /**
+ * Return an instance with the specified query string arguments.
+ *
+ * These values SHOULD remain immutable over the course of the incoming
+ * request. They MAY be injected during instantiation, such as from PHP's
+ * $_GET superglobal, or MAY be derived from some other value such as the
+ * URI. In cases where the arguments are parsed from the URI, the data
+ * MUST be compatible with what PHP's parse_str() would return for
+ * purposes of how duplicate query parameters are handled, and how nested
+ * sets are handled.
+ *
+ * Setting query string arguments MUST NOT change the URI stored by the
+ * request, nor the values in the server params.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated query string arguments.
+ *
+ * @param array $query Array of query string arguments, typically from
+ * $_GET.
+ * @return static
+ */
+ public function withQueryParams(array $query);
+
+ /**
+ * Retrieve normalized file upload data.
+ *
+ * This method returns upload metadata in a normalized tree, with each leaf
+ * an instance of Psr\Http\Message\UploadedFileInterface.
+ *
+ * These values MAY be prepared from $_FILES or the message body during
+ * instantiation, or MAY be injected via withUploadedFiles().
+ *
+ * @return array An array tree of UploadedFileInterface instances; an empty
+ * array MUST be returned if no data is present.
+ */
+ public function getUploadedFiles();
+
+ /**
+ * Create a new instance with the specified uploaded files.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated body parameters.
+ *
+ * @param array $uploadedFiles An array tree of UploadedFileInterface instances.
+ * @return static
+ * @throws \InvalidArgumentException if an invalid structure is provided.
+ */
+ public function withUploadedFiles(array $uploadedFiles);
+
+ /**
+ * Retrieve any parameters provided in the request body.
+ *
+ * If the request Content-Type is either application/x-www-form-urlencoded
+ * or multipart/form-data, and the request method is POST, this method MUST
+ * return the contents of $_POST.
+ *
+ * Otherwise, this method may return any results of deserializing
+ * the request body content; as parsing returns structured content, the
+ * potential types MUST be arrays or objects only. A null value indicates
+ * the absence of body content.
+ *
+ * @return null|array|object The deserialized body parameters, if any.
+ * These will typically be an array or object.
+ */
+ public function getParsedBody();
+
+ /**
+ * Return an instance with the specified body parameters.
+ *
+ * These MAY be injected during instantiation.
+ *
+ * If the request Content-Type is either application/x-www-form-urlencoded
+ * or multipart/form-data, and the request method is POST, use this method
+ * ONLY to inject the contents of $_POST.
+ *
+ * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of
+ * deserializing the request body content. Deserialization/parsing returns
+ * structured data, and, as such, this method ONLY accepts arrays or objects,
+ * or a null value if nothing was available to parse.
+ *
+ * As an example, if content negotiation determines that the request data
+ * is a JSON payload, this method could be used to create a request
+ * instance with the deserialized parameters.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated body parameters.
+ *
+ * @param null|array|object $data The deserialized body data. This will
+ * typically be in an array or object.
+ * @return static
+ * @throws \InvalidArgumentException if an unsupported argument type is
+ * provided.
+ */
+ public function withParsedBody($data);
+
+ /**
+ * Retrieve attributes derived from the request.
+ *
+ * The request "attributes" may be used to allow injection of any
+ * parameters derived from the request: e.g., the results of path
+ * match operations; the results of decrypting cookies; the results of
+ * deserializing non-form-encoded message bodies; etc. Attributes
+ * will be application and request specific, and CAN be mutable.
+ *
+ * @return array Attributes derived from the request.
+ */
+ public function getAttributes();
+
+ /**
+ * Retrieve a single derived request attribute.
+ *
+ * Retrieves a single derived request attribute as described in
+ * getAttributes(). If the attribute has not been previously set, returns
+ * the default value as provided.
+ *
+ * This method obviates the need for a hasAttribute() method, as it allows
+ * specifying a default value to return if the attribute is not found.
+ *
+ * @see getAttributes()
+ * @param string $name The attribute name.
+ * @param mixed $default Default value to return if the attribute does not exist.
+ * @return mixed
+ */
+ public function getAttribute($name, $default = null);
+
+ /**
+ * Return an instance with the specified derived request attribute.
+ *
+ * This method allows setting a single derived request attribute as
+ * described in getAttributes().
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated attribute.
+ *
+ * @see getAttributes()
+ * @param string $name The attribute name.
+ * @param mixed $value The value of the attribute.
+ * @return static
+ */
+ public function withAttribute($name, $value);
+
+ /**
+ * Return an instance that removes the specified derived request attribute.
+ *
+ * This method allows removing a single derived request attribute as
+ * described in getAttributes().
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that removes
+ * the attribute.
+ *
+ * @see getAttributes()
+ * @param string $name The attribute name.
+ * @return static
+ */
+ public function withoutAttribute($name);
+ }
+}
+
+// file: vendor/psr/http-message/src/StreamInterface.php
+namespace Psr\Http\Message {
+
+ /**
+ * Describes a data stream.
+ *
+ * Typically, an instance will wrap a PHP stream; this interface provides
+ * a wrapper around the most common operations, including serialization of
+ * the entire stream to a string.
+ */
+ interface StreamInterface
+ {
+ /**
+ * Reads all data from the stream into a string, from the beginning to end.
+ *
+ * This method MUST attempt to seek to the beginning of the stream before
+ * reading data and read the stream until the end is reached.
+ *
+ * Warning: This could attempt to load a large amount of data into memory.
+ *
+ * This method MUST NOT raise an exception in order to conform with PHP's
+ * string casting operations.
+ *
+ * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
+ * @return string
+ */
+ public function __toString();
+
+ /**
+ * Closes the stream and any underlying resources.
+ *
+ * @return void
+ */
+ public function close();
+
+ /**
+ * Separates any underlying resources from the stream.
+ *
+ * After the stream has been detached, the stream is in an unusable state.
+ *
+ * @return resource|null Underlying PHP stream, if any
+ */
+ public function detach();
+
+ /**
+ * Get the size of the stream if known.
+ *
+ * @return int|null Returns the size in bytes if known, or null if unknown.
+ */
+ public function getSize();
+
+ /**
+ * Returns the current position of the file read/write pointer
+ *
+ * @return int Position of the file pointer
+ * @throws \RuntimeException on error.
+ */
+ public function tell();
+
+ /**
+ * Returns true if the stream is at the end of the stream.
+ *
+ * @return bool
+ */
+ public function eof();
+
+ /**
+ * Returns whether or not the stream is seekable.
+ *
+ * @return bool
+ */
+ public function isSeekable();
+
+ /**
+ * Seek to a position in the stream.
+ *
+ * @link http://www.php.net/manual/en/function.fseek.php
+ * @param int $offset Stream offset
+ * @param int $whence Specifies how the cursor position will be calculated
+ * based on the seek offset. Valid values are identical to the built-in
+ * PHP $whence values for `fseek()`. SEEK_SET: Set position equal to
+ * offset bytes SEEK_CUR: Set position to current location plus offset
+ * SEEK_END: Set position to end-of-stream plus offset.
+ * @throws \RuntimeException on failure.
+ */
+ public function seek($offset, $whence = SEEK_SET);
+
+ /**
+ * Seek to the beginning of the stream.
+ *
+ * If the stream is not seekable, this method will raise an exception;
+ * otherwise, it will perform a seek(0).
+ *
+ * @see seek()
+ * @link http://www.php.net/manual/en/function.fseek.php
+ * @throws \RuntimeException on failure.
+ */
+ public function rewind();
+
+ /**
+ * Returns whether or not the stream is writable.
+ *
+ * @return bool
+ */
+ public function isWritable();
+
+ /**
+ * Write data to the stream.
+ *
+ * @param string $string The string that is to be written.
+ * @return int Returns the number of bytes written to the stream.
+ * @throws \RuntimeException on failure.
+ */
+ public function write($string);
+
+ /**
+ * Returns whether or not the stream is readable.
+ *
+ * @return bool
+ */
+ public function isReadable();
+
+ /**
+ * Read data from the stream.
+ *
+ * @param int $length Read up to $length bytes from the object and return
+ * them. Fewer than $length bytes may be returned if underlying stream
+ * call returns fewer bytes.
+ * @return string Returns the data read from the stream, or an empty string
+ * if no bytes are available.
+ * @throws \RuntimeException if an error occurs.
+ */
+ public function read($length);
+
+ /**
+ * Returns the remaining contents in a string
+ *
+ * @return string
+ * @throws \RuntimeException if unable to read or an error occurs while
+ * reading.
+ */
+ public function getContents();
+
+ /**
+ * Get stream metadata as an associative array or retrieve a specific key.
+ *
+ * The keys returned are identical to the keys returned from PHP's
+ * stream_get_meta_data() function.
+ *
+ * @link http://php.net/manual/en/function.stream-get-meta-data.php
+ * @param string $key Specific metadata to retrieve.
+ * @return array|mixed|null Returns an associative array if no key is
+ * provided. Returns a specific key value if a key is provided and the
+ * value is found, or null if the key is not found.
+ */
+ public function getMetadata($key = null);
+ }
+}
+
+// file: vendor/psr/http-message/src/UploadedFileInterface.php
+namespace Psr\Http\Message {
+
+ /**
+ * Value object representing a file uploaded through an HTTP request.
+ *
+ * Instances of this interface are considered immutable; all methods that
+ * might change state MUST be implemented such that they retain the internal
+ * state of the current instance and return an instance that contains the
+ * changed state.
+ */
+ interface UploadedFileInterface
+ {
+ /**
+ * Retrieve a stream representing the uploaded file.
+ *
+ * This method MUST return a StreamInterface instance, representing the
+ * uploaded file. The purpose of this method is to allow utilizing native PHP
+ * stream functionality to manipulate the file upload, such as
+ * stream_copy_to_stream() (though the result will need to be decorated in a
+ * native PHP stream wrapper to work with such functions).
+ *
+ * If the moveTo() method has been called previously, this method MUST raise
+ * an exception.
+ *
+ * @return StreamInterface Stream representation of the uploaded file.
+ * @throws \RuntimeException in cases when no stream is available or can be
+ * created.
+ */
+ public function getStream();
+
+ /**
+ * Move the uploaded file to a new location.
+ *
+ * Use this method as an alternative to move_uploaded_file(). This method is
+ * guaranteed to work in both SAPI and non-SAPI environments.
+ * Implementations must determine which environment they are in, and use the
+ * appropriate method (move_uploaded_file(), rename(), or a stream
+ * operation) to perform the operation.
+ *
+ * $targetPath may be an absolute path, or a relative path. If it is a
+ * relative path, resolution should be the same as used by PHP's rename()
+ * function.
+ *
+ * The original file or stream MUST be removed on completion.
+ *
+ * If this method is called more than once, any subsequent calls MUST raise
+ * an exception.
+ *
+ * When used in an SAPI environment where $_FILES is populated, when writing
+ * files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be
+ * used to ensure permissions and upload status are verified correctly.
+ *
+ * If you wish to move to a stream, use getStream(), as SAPI operations
+ * cannot guarantee writing to stream destinations.
+ *
+ * @see http://php.net/is_uploaded_file
+ * @see http://php.net/move_uploaded_file
+ * @param string $targetPath Path to which to move the uploaded file.
+ * @throws \InvalidArgumentException if the $targetPath specified is invalid.
+ * @throws \RuntimeException on any error during the move operation, or on
+ * the second or subsequent call to the method.
+ */
+ public function moveTo($targetPath);
+
+ /**
+ * Retrieve the file size.
+ *
+ * Implementations SHOULD return the value stored in the "size" key of
+ * the file in the $_FILES array if available, as PHP calculates this based
+ * on the actual size transmitted.
+ *
+ * @return int|null The file size in bytes or null if unknown.
+ */
+ public function getSize();
+
+ /**
+ * Retrieve the error associated with the uploaded file.
+ *
+ * The return value MUST be one of PHP's UPLOAD_ERR_XXX constants.
+ *
+ * If the file was uploaded successfully, this method MUST return
+ * UPLOAD_ERR_OK.
+ *
+ * Implementations SHOULD return the value stored in the "error" key of
+ * the file in the $_FILES array.
+ *
+ * @see http://php.net/manual/en/features.file-upload.errors.php
+ * @return int One of PHP's UPLOAD_ERR_XXX constants.
+ */
+ public function getError();
+
+ /**
+ * Retrieve the filename sent by the client.
+ *
+ * Do not trust the value returned by this method. A client could send
+ * a malicious filename with the intention to corrupt or hack your
+ * application.
+ *
+ * Implementations SHOULD return the value stored in the "name" key of
+ * the file in the $_FILES array.
+ *
+ * @return string|null The filename sent by the client or null if none
+ * was provided.
+ */
+ public function getClientFilename();
+
+ /**
+ * Retrieve the media type sent by the client.
+ *
+ * Do not trust the value returned by this method. A client could send
+ * a malicious media type with the intention to corrupt or hack your
+ * application.
+ *
+ * Implementations SHOULD return the value stored in the "type" key of
+ * the file in the $_FILES array.
+ *
+ * @return string|null The media type sent by the client or null if none
+ * was provided.
+ */
+ public function getClientMediaType();
+ }
+}
+
+// file: vendor/psr/http-message/src/UriInterface.php
+namespace Psr\Http\Message {
+
+ /**
+ * Value object representing a URI.
+ *
+ * This interface is meant to represent URIs according to RFC 3986 and to
+ * provide methods for most common operations. Additional functionality for
+ * working with URIs can be provided on top of the interface or externally.
+ * Its primary use is for HTTP requests, but may also be used in other
+ * contexts.
+ *
+ * Instances of this interface are considered immutable; all methods that
+ * might change state MUST be implemented such that they retain the internal
+ * state of the current instance and return an instance that contains the
+ * changed state.
+ *
+ * Typically the Host header will be also be present in the request message.
+ * For server-side requests, the scheme will typically be discoverable in the
+ * server parameters.
+ *
+ * @link http://tools.ietf.org/html/rfc3986 (the URI specification)
+ */
+ interface UriInterface
+ {
+ /**
+ * Retrieve the scheme component of the URI.
+ *
+ * If no scheme is present, this method MUST return an empty string.
+ *
+ * The value returned MUST be normalized to lowercase, per RFC 3986
+ * Section 3.1.
+ *
+ * The trailing ":" character is not part of the scheme and MUST NOT be
+ * added.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-3.1
+ * @return string The URI scheme.
+ */
+ public function getScheme();
+
+ /**
+ * Retrieve the authority component of the URI.
+ *
+ * If no authority information is present, this method MUST return an empty
+ * string.
+ *
+ * The authority syntax of the URI is:
+ *
+ *
+ * [user-info@]host[:port]
+ *
+ *
+ * If the port component is not set or is the standard port for the current
+ * scheme, it SHOULD NOT be included.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-3.2
+ * @return string The URI authority, in "[user-info@]host[:port]" format.
+ */
+ public function getAuthority();
+
+ /**
+ * Retrieve the user information component of the URI.
+ *
+ * If no user information is present, this method MUST return an empty
+ * string.
+ *
+ * If a user is present in the URI, this will return that value;
+ * additionally, if the password is also present, it will be appended to the
+ * user value, with a colon (":") separating the values.
+ *
+ * The trailing "@" character is not part of the user information and MUST
+ * NOT be added.
+ *
+ * @return string The URI user information, in "username[:password]" format.
+ */
+ public function getUserInfo();
+
+ /**
+ * Retrieve the host component of the URI.
+ *
+ * If no host is present, this method MUST return an empty string.
+ *
+ * The value returned MUST be normalized to lowercase, per RFC 3986
+ * Section 3.2.2.
+ *
+ * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
+ * @return string The URI host.
+ */
+ public function getHost();
+
+ /**
+ * Retrieve the port component of the URI.
+ *
+ * If a port is present, and it is non-standard for the current scheme,
+ * this method MUST return it as an integer. If the port is the standard port
+ * used with the current scheme, this method SHOULD return null.
+ *
+ * If no port is present, and no scheme is present, this method MUST return
+ * a null value.
+ *
+ * If no port is present, but a scheme is present, this method MAY return
+ * the standard port for that scheme, but SHOULD return null.
+ *
+ * @return null|int The URI port.
+ */
+ public function getPort();
+
+ /**
+ * Retrieve the path component of the URI.
+ *
+ * The path can either be empty or absolute (starting with a slash) or
+ * rootless (not starting with a slash). Implementations MUST support all
+ * three syntaxes.
+ *
+ * Normally, the empty path "" and absolute path "/" are considered equal as
+ * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
+ * do this normalization because in contexts with a trimmed base path, e.g.
+ * the front controller, this difference becomes significant. It's the task
+ * of the user to handle both "" and "/".
+ *
+ * The value returned MUST be percent-encoded, but MUST NOT double-encode
+ * any characters. To determine what characters to encode, please refer to
+ * RFC 3986, Sections 2 and 3.3.
+ *
+ * As an example, if the value should include a slash ("/") not intended as
+ * delimiter between path segments, that value MUST be passed in encoded
+ * form (e.g., "%2F") to the instance.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-2
+ * @see https://tools.ietf.org/html/rfc3986#section-3.3
+ * @return string The URI path.
+ */
+ public function getPath();
+
+ /**
+ * Retrieve the query string of the URI.
+ *
+ * If no query string is present, this method MUST return an empty string.
+ *
+ * The leading "?" character is not part of the query and MUST NOT be
+ * added.
+ *
+ * The value returned MUST be percent-encoded, but MUST NOT double-encode
+ * any characters. To determine what characters to encode, please refer to
+ * RFC 3986, Sections 2 and 3.4.
+ *
+ * As an example, if a value in a key/value pair of the query string should
+ * include an ampersand ("&") not intended as a delimiter between values,
+ * that value MUST be passed in encoded form (e.g., "%26") to the instance.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-2
+ * @see https://tools.ietf.org/html/rfc3986#section-3.4
+ * @return string The URI query string.
+ */
+ public function getQuery();
+
+ /**
+ * Retrieve the fragment component of the URI.
+ *
+ * If no fragment is present, this method MUST return an empty string.
+ *
+ * The leading "#" character is not part of the fragment and MUST NOT be
+ * added.
+ *
+ * The value returned MUST be percent-encoded, but MUST NOT double-encode
+ * any characters. To determine what characters to encode, please refer to
+ * RFC 3986, Sections 2 and 3.5.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-2
+ * @see https://tools.ietf.org/html/rfc3986#section-3.5
+ * @return string The URI fragment.
+ */
+ public function getFragment();
+
+ /**
+ * Return an instance with the specified scheme.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified scheme.
+ *
+ * Implementations MUST support the schemes "http" and "https" case
+ * insensitively, and MAY accommodate other schemes if required.
+ *
+ * An empty scheme is equivalent to removing the scheme.
+ *
+ * @param string $scheme The scheme to use with the new instance.
+ * @return static A new instance with the specified scheme.
+ * @throws \InvalidArgumentException for invalid or unsupported schemes.
+ */
+ public function withScheme($scheme);
+
+ /**
+ * Return an instance with the specified user information.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified user information.
+ *
+ * Password is optional, but the user information MUST include the
+ * user; an empty string for the user is equivalent to removing user
+ * information.
+ *
+ * @param string $user The user name to use for authority.
+ * @param null|string $password The password associated with $user.
+ * @return static A new instance with the specified user information.
+ */
+ public function withUserInfo($user, $password = null);
+
+ /**
+ * Return an instance with the specified host.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified host.
+ *
+ * An empty host value is equivalent to removing the host.
+ *
+ * @param string $host The hostname to use with the new instance.
+ * @return static A new instance with the specified host.
+ * @throws \InvalidArgumentException for invalid hostnames.
+ */
+ public function withHost($host);
+
+ /**
+ * Return an instance with the specified port.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified port.
+ *
+ * Implementations MUST raise an exception for ports outside the
+ * established TCP and UDP port ranges.
+ *
+ * A null value provided for the port is equivalent to removing the port
+ * information.
+ *
+ * @param null|int $port The port to use with the new instance; a null value
+ * removes the port information.
+ * @return static A new instance with the specified port.
+ * @throws \InvalidArgumentException for invalid ports.
+ */
+ public function withPort($port);
+
+ /**
+ * Return an instance with the specified path.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified path.
+ *
+ * The path can either be empty or absolute (starting with a slash) or
+ * rootless (not starting with a slash). Implementations MUST support all
+ * three syntaxes.
+ *
+ * If the path is intended to be domain-relative rather than path relative then
+ * it must begin with a slash ("/"). Paths not starting with a slash ("/")
+ * are assumed to be relative to some base path known to the application or
+ * consumer.
+ *
+ * Users can provide both encoded and decoded path characters.
+ * Implementations ensure the correct encoding as outlined in getPath().
+ *
+ * @param string $path The path to use with the new instance.
+ * @return static A new instance with the specified path.
+ * @throws \InvalidArgumentException for invalid paths.
+ */
+ public function withPath($path);
+
+ /**
+ * Return an instance with the specified query string.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified query string.
+ *
+ * Users can provide both encoded and decoded query characters.
+ * Implementations ensure the correct encoding as outlined in getQuery().
+ *
+ * An empty query string value is equivalent to removing the query string.
+ *
+ * @param string $query The query string to use with the new instance.
+ * @return static A new instance with the specified query string.
+ * @throws \InvalidArgumentException for invalid query strings.
+ */
+ public function withQuery($query);
+
+ /**
+ * Return an instance with the specified URI fragment.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified URI fragment.
+ *
+ * Users can provide both encoded and decoded fragment characters.
+ * Implementations ensure the correct encoding as outlined in getFragment().
+ *
+ * An empty fragment value is equivalent to removing the fragment.
+ *
+ * @param string $fragment The fragment to use with the new instance.
+ * @return static A new instance with the specified fragment.
+ */
+ public function withFragment($fragment);
+
+ /**
+ * Return the string representation as a URI reference.
+ *
+ * Depending on which components of the URI are present, the resulting
+ * string is either a full URI or relative reference according to RFC 3986,
+ * Section 4.1. The method concatenates the various components of the URI,
+ * using the appropriate delimiters:
+ *
+ * - If a scheme is present, it MUST be suffixed by ":".
+ * - If an authority is present, it MUST be prefixed by "//".
+ * - The path can be concatenated without delimiters. But there are two
+ * cases where the path has to be adjusted to make the URI reference
+ * valid as PHP does not allow to throw an exception in __toString():
+ * - If the path is rootless and an authority is present, the path MUST
+ * be prefixed by "/".
+ * - If the path is starting with more than one "/" and no authority is
+ * present, the starting slashes MUST be reduced to one.
+ * - If a query is present, it MUST be prefixed by "?".
+ * - If a fragment is present, it MUST be prefixed by "#".
+ *
+ * @see http://tools.ietf.org/html/rfc3986#section-4.1
+ * @return string
+ */
+ public function __toString();
+ }
+}
+
+// file: vendor/psr/http-server-handler/src/RequestHandlerInterface.php
+namespace Psr\Http\Server {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+
+ /**
+ * Handles a server request and produces a response.
+ *
+ * An HTTP request handler process an HTTP request in order to produce an
+ * HTTP response.
+ */
+ interface RequestHandlerInterface
+ {
+ /**
+ * Handles a request and produces a response.
+ *
+ * May call other collaborating code to generate the response.
+ */
+ public function handle(ServerRequestInterface $request): ResponseInterface;
+ }
+}
+
+// file: vendor/psr/http-server-middleware/src/MiddlewareInterface.php
+namespace Psr\Http\Server {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+
+ /**
+ * Participant in processing a server request and response.
+ *
+ * An HTTP middleware component participates in processing an HTTP message:
+ * by acting on the request, generating the response, or forwarding the
+ * request to a subsequent middleware and possibly acting on its response.
+ */
+ interface MiddlewareInterface
+ {
+ /**
+ * Process an incoming server request.
+ *
+ * Processes an incoming server request in order to produce a response.
+ * If unable to produce the response itself, it may delegate to the provided
+ * request handler to do so.
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface;
+ }
+}
+
+// file: vendor/nyholm/psr7/src/Factory/Psr17Factory.php
+namespace Nyholm\Psr7\Factory {
+
+ use Nyholm\Psr7\{Request, Response, ServerRequest, Stream, UploadedFile, Uri};
+ use Psr\Http\Message\{RequestFactoryInterface, RequestInterface, ResponseFactoryInterface, ResponseInterface, ServerRequestFactoryInterface, ServerRequestInterface, StreamFactoryInterface, StreamInterface, UploadedFileFactoryInterface, UploadedFileInterface, UriFactoryInterface, UriInterface};
+
+ /**
+ * @author Tobias Nyholm
+ * @author Martijn van der Ven
+ */
+ final class Psr17Factory implements RequestFactoryInterface, ResponseFactoryInterface, ServerRequestFactoryInterface, StreamFactoryInterface, UploadedFileFactoryInterface, UriFactoryInterface
+ {
+ public function createRequest(string $method, $uri): RequestInterface
+ {
+ return new Request($method, $uri);
+ }
+
+ public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface
+ {
+ if (2 > \func_num_args()) {
+ // This will make the Response class to use a custom reasonPhrase
+ $reasonPhrase = null;
+ }
+
+ return new Response($code, [], null, '1.1', $reasonPhrase);
+ }
+
+ public function createStream(string $content = ''): StreamInterface
+ {
+ return Stream::create($content);
+ }
+
+ public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface
+ {
+ $resource = @\fopen($filename, $mode);
+ if (false === $resource) {
+ if ('' === $mode || false === \in_array($mode[0], ['r', 'w', 'a', 'x', 'c'])) {
+ throw new \InvalidArgumentException('The mode ' . $mode . ' is invalid.');
+ }
+
+ throw new \RuntimeException('The file ' . $filename . ' cannot be opened.');
+ }
+
+ return Stream::create($resource);
+ }
+
+ public function createStreamFromResource($resource): StreamInterface
+ {
+ return Stream::create($resource);
+ }
+
+ public function createUploadedFile(StreamInterface $stream, int $size = null, int $error = \UPLOAD_ERR_OK, string $clientFilename = null, string $clientMediaType = null): UploadedFileInterface
+ {
+ if (null === $size) {
+ $size = $stream->getSize();
+ }
+
+ return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType);
+ }
+
+ public function createUri(string $uri = ''): UriInterface
+ {
+ return new Uri($uri);
+ }
+
+ public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
+ {
+ return new ServerRequest($method, $uri, [], null, '1.1', $serverParams);
+ }
+ }
+}
+
+// file: vendor/nyholm/psr7/src/MessageTrait.php
+namespace Nyholm\Psr7 {
+
+ use Psr\Http\Message\StreamInterface;
+
+ /**
+ * Trait implementing functionality common to requests and responses.
+ *
+ * @author Michael Dowling and contributors to guzzlehttp/psr7
+ * @author Tobias Nyholm
+ * @author Martijn van der Ven
+ *
+ * @internal should not be used outside of Nyholm/Psr7 as it does not fall under our BC promise
+ */
+ trait MessageTrait
+ {
+ /** @var array Map of all registered headers, as original name => array of values */
+ private $headers = [];
+
+ /** @var array Map of lowercase header name => original name at registration */
+ private $headerNames = [];
+
+ /** @var string */
+ private $protocol = '1.1';
+
+ /** @var StreamInterface|null */
+ private $stream;
+
+ public function getProtocolVersion(): string
+ {
+ return $this->protocol;
+ }
+
+ public function withProtocolVersion($version): self
+ {
+ if ($this->protocol === $version) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->protocol = $version;
+
+ return $new;
+ }
+
+ public function getHeaders(): array
+ {
+ return $this->headers;
+ }
+
+ public function hasHeader($header): bool
+ {
+ return isset($this->headerNames[\strtolower($header)]);
+ }
+
+ public function getHeader($header): array
+ {
+ $header = \strtolower($header);
+ if (!isset($this->headerNames[$header])) {
+ return [];
+ }
+
+ $header = $this->headerNames[$header];
+
+ return $this->headers[$header];
+ }
+
+ public function getHeaderLine($header): string
+ {
+ return \implode(', ', $this->getHeader($header));
+ }
+
+ public function withHeader($header, $value): self
+ {
+ $value = $this->validateAndTrimHeader($header, $value);
+ $normalized = \strtolower($header);
+
+ $new = clone $this;
+ if (isset($new->headerNames[$normalized])) {
+ unset($new->headers[$new->headerNames[$normalized]]);
+ }
+ $new->headerNames[$normalized] = $header;
+ $new->headers[$header] = $value;
+
+ return $new;
+ }
+
+ public function withAddedHeader($header, $value): self
+ {
+ if (!\is_string($header) || '' === $header) {
+ throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string.');
+ }
+
+ $new = clone $this;
+ $new->setHeaders([$header => $value]);
+
+ return $new;
+ }
+
+ public function withoutHeader($header): self
+ {
+ $normalized = \strtolower($header);
+ if (!isset($this->headerNames[$normalized])) {
+ return $this;
+ }
+
+ $header = $this->headerNames[$normalized];
+ $new = clone $this;
+ unset($new->headers[$header], $new->headerNames[$normalized]);
+
+ return $new;
+ }
+
+ public function getBody(): StreamInterface
+ {
+ if (null === $this->stream) {
+ $this->stream = Stream::create('');
+ }
+
+ return $this->stream;
+ }
+
+ public function withBody(StreamInterface $body): self
+ {
+ if ($body === $this->stream) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->stream = $body;
+
+ return $new;
+ }
+
+ private function setHeaders(array $headers) /*:void*/
+ {
+ foreach ($headers as $header => $value) {
+ $value = $this->validateAndTrimHeader($header, $value);
+ $normalized = \strtolower($header);
+ if (isset($this->headerNames[$normalized])) {
+ $header = $this->headerNames[$normalized];
+ $this->headers[$header] = \array_merge($this->headers[$header], $value);
+ } else {
+ $this->headerNames[$normalized] = $header;
+ $this->headers[$header] = $value;
+ }
+ }
+ }
+
+ /**
+ * Make sure the header complies with RFC 7230.
+ *
+ * Header names must be a non-empty string consisting of token characters.
+ *
+ * Header values must be strings consisting of visible characters with all optional
+ * leading and trailing whitespace stripped. This method will always strip such
+ * optional whitespace. Note that the method does not allow folding whitespace within
+ * the values as this was deprecated for almost all instances by the RFC.
+ *
+ * header-field = field-name ":" OWS field-value OWS
+ * field-name = 1*( "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^"
+ * / "_" / "`" / "|" / "~" / %x30-39 / ( %x41-5A / %x61-7A ) )
+ * OWS = *( SP / HTAB )
+ * field-value = *( ( %x21-7E / %x80-FF ) [ 1*( SP / HTAB ) ( %x21-7E / %x80-FF ) ] )
+ *
+ * @see https://tools.ietf.org/html/rfc7230#section-3.2.4
+ */
+ private function validateAndTrimHeader($header, $values): array
+ {
+ if (!\is_string($header) || 1 !== \preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@", $header)) {
+ throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string.');
+ }
+
+ if (!\is_array($values)) {
+ // This is simple, just one value.
+ if ((!\is_numeric($values) && !\is_string($values)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $values)) {
+ throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings.');
+ }
+
+ return [\trim((string) $values, " \t")];
+ }
+
+ if (empty($values)) {
+ throw new \InvalidArgumentException('Header values must be a string or an array of strings, empty array given.');
+ }
+
+ // Assert Non empty array
+ $returnValues = [];
+ foreach ($values as $v) {
+ if ((!\is_numeric($v) && !\is_string($v)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $v)) {
+ throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings.');
+ }
+
+ $returnValues[] = \trim((string) $v, " \t");
+ }
+
+ return $returnValues;
+ }
+ }
+}
+
+// file: vendor/nyholm/psr7/src/Request.php
+namespace Nyholm\Psr7 {
+
+ use Psr\Http\Message\{RequestInterface, StreamInterface, UriInterface};
+
+ /**
+ * @author Tobias Nyholm
+ * @author Martijn van der Ven
+ */
+ final class Request implements RequestInterface
+ {
+ use MessageTrait;
+ use RequestTrait;
+
+ /**
+ * @param string $method HTTP method
+ * @param string|UriInterface $uri URI
+ * @param array $headers Request headers
+ * @param string|resource|StreamInterface|null $body Request body
+ * @param string $version Protocol version
+ */
+ public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1')
+ {
+ if (!($uri instanceof UriInterface)) {
+ $uri = new Uri($uri);
+ }
+
+ $this->method = $method;
+ $this->uri = $uri;
+ $this->setHeaders($headers);
+ $this->protocol = $version;
+
+ if (!$this->hasHeader('Host')) {
+ $this->updateHostFromUri();
+ }
+
+ // If we got no body, defer initialization of the stream until Request::getBody()
+ if ('' !== $body && null !== $body) {
+ $this->stream = Stream::create($body);
+ }
+ }
+ }
+}
+
+// file: vendor/nyholm/psr7/src/RequestTrait.php
+namespace Nyholm\Psr7 {
+
+ use Psr\Http\Message\UriInterface;
+
+ /**
+ * @author Michael Dowling and contributors to guzzlehttp/psr7
+ * @author Tobias Nyholm
+ * @author Martijn van der Ven
+ *
+ * @internal should not be used outside of Nyholm/Psr7 as it does not fall under our BC promise
+ */
+ trait RequestTrait
+ {
+ /** @var string */
+ private $method;
+
+ /** @var string|null */
+ private $requestTarget;
+
+ /** @var UriInterface|null */
+ private $uri;
+
+ public function getRequestTarget(): string
+ {
+ if (null !== $this->requestTarget) {
+ return $this->requestTarget;
+ }
+
+ if ('' === $target = $this->uri->getPath()) {
+ $target = '/';
+ }
+ if ('' !== $this->uri->getQuery()) {
+ $target .= '?' . $this->uri->getQuery();
+ }
+
+ return $target;
+ }
+
+ public function withRequestTarget($requestTarget): self
+ {
+ if (\preg_match('#\s#', $requestTarget)) {
+ throw new \InvalidArgumentException('Invalid request target provided; cannot contain whitespace');
+ }
+
+ $new = clone $this;
+ $new->requestTarget = $requestTarget;
+
+ return $new;
+ }
+
+ public function getMethod(): string
+ {
+ return $this->method;
+ }
+
+ public function withMethod($method): self
+ {
+ if (!\is_string($method)) {
+ throw new \InvalidArgumentException('Method must be a string');
+ }
+
+ $new = clone $this;
+ $new->method = $method;
+
+ return $new;
+ }
+
+ public function getUri(): UriInterface
+ {
+ return $this->uri;
+ }
+
+ public function withUri(UriInterface $uri, $preserveHost = false): self
+ {
+ if ($uri === $this->uri) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->uri = $uri;
+
+ if (!$preserveHost || !$this->hasHeader('Host')) {
+ $new->updateHostFromUri();
+ }
+
+ return $new;
+ }
+
+ private function updateHostFromUri() /*:void*/
+ {
+ if ('' === $host = $this->uri->getHost()) {
+ return;
+ }
+
+ if (null !== ($port = $this->uri->getPort())) {
+ $host .= ':' . $port;
+ }
+
+ if (isset($this->headerNames['host'])) {
+ $header = $this->headerNames['host'];
+ } else {
+ $this->headerNames['host'] = $header = 'Host';
+ }
+
+ // Ensure Host is the first header.
+ // See: http://tools.ietf.org/html/rfc7230#section-5.4
+ $this->headers = [$header => [$host]] + $this->headers;
+ }
+ }
+}
+
+// file: vendor/nyholm/psr7/src/Response.php
+namespace Nyholm\Psr7 {
+
+ use Psr\Http\Message\{ResponseInterface, StreamInterface};
+
+ /**
+ * @author Michael Dowling and contributors to guzzlehttp/psr7
+ * @author Tobias Nyholm
+ * @author Martijn van der Ven
+ */
+ final class Response implements ResponseInterface
+ {
+ use MessageTrait;
+
+ /** @var array Map of standard HTTP status code/reason phrases */
+ /*private*/ const PHRASES = [
+ 100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing',
+ 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-status', 208 => 'Already Reported',
+ 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 306 => 'Switch Proxy', 307 => 'Temporary Redirect',
+ 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed', 418 => 'I\'m a teapot', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 425 => 'Unordered Collection', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 451 => 'Unavailable For Legal Reasons',
+ 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', 511 => 'Network Authentication Required',
+ ];
+
+ /** @var string */
+ private $reasonPhrase = '';
+
+ /** @var int */
+ private $statusCode;
+
+ /**
+ * @param int $status Status code
+ * @param array $headers Response headers
+ * @param string|resource|StreamInterface|null $body Response body
+ * @param string $version Protocol version
+ * @param string|null $reason Reason phrase (when empty a default will be used based on the status code)
+ */
+ public function __construct(int $status = 200, array $headers = [], $body = null, string $version = '1.1', string $reason = null)
+ {
+ // If we got no body, defer initialization of the stream until Response::getBody()
+ if ('' !== $body && null !== $body) {
+ $this->stream = Stream::create($body);
+ }
+
+ $this->statusCode = $status;
+ $this->setHeaders($headers);
+ if (null === $reason && isset(self::PHRASES[$this->statusCode])) {
+ $this->reasonPhrase = self::PHRASES[$status];
+ } else {
+ $this->reasonPhrase = $reason ?? '';
+ }
+
+ $this->protocol = $version;
+ }
+
+ public function getStatusCode(): int
+ {
+ return $this->statusCode;
+ }
+
+ public function getReasonPhrase(): string
+ {
+ return $this->reasonPhrase;
+ }
+
+ public function withStatus($code, $reasonPhrase = ''): self
+ {
+ if (!\is_int($code) && !\is_string($code)) {
+ throw new \InvalidArgumentException('Status code has to be an integer');
+ }
+
+ $code = (int) $code;
+ if ($code < 100 || $code > 599) {
+ throw new \InvalidArgumentException('Status code has to be an integer between 100 and 599');
+ }
+
+ $new = clone $this;
+ $new->statusCode = $code;
+ if ((null === $reasonPhrase || '' === $reasonPhrase) && isset(self::PHRASES[$new->statusCode])) {
+ $reasonPhrase = self::PHRASES[$new->statusCode];
+ }
+ $new->reasonPhrase = $reasonPhrase;
+
+ return $new;
+ }
+ }
+}
+
+// file: vendor/nyholm/psr7/src/ServerRequest.php
+namespace Nyholm\Psr7 {
+
+ use Psr\Http\Message\{ServerRequestInterface, StreamInterface, UploadedFileInterface, UriInterface};
+
+ /**
+ * @author Michael Dowling and contributors to guzzlehttp/psr7
+ * @author Tobias Nyholm
+ * @author Martijn van der Ven
+ */
+ final class ServerRequest implements ServerRequestInterface
+ {
+ use MessageTrait;
+ use RequestTrait;
+
+ /** @var array */
+ private $attributes = [];
+
+ /** @var array */
+ private $cookieParams = [];
+
+ /** @var array|object|null */
+ private $parsedBody;
+
+ /** @var array */
+ private $queryParams = [];
+
+ /** @var array */
+ private $serverParams;
+
+ /** @var UploadedFileInterface[] */
+ private $uploadedFiles = [];
+
+ /**
+ * @param string $method HTTP method
+ * @param string|UriInterface $uri URI
+ * @param array $headers Request headers
+ * @param string|resource|StreamInterface|null $body Request body
+ * @param string $version Protocol version
+ * @param array $serverParams Typically the $_SERVER superglobal
+ */
+ public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1', array $serverParams = [])
+ {
+ $this->serverParams = $serverParams;
+
+ if (!($uri instanceof UriInterface)) {
+ $uri = new Uri($uri);
+ }
+
+ $this->method = $method;
+ $this->uri = $uri;
+ $this->setHeaders($headers);
+ $this->protocol = $version;
+
+ if (!$this->hasHeader('Host')) {
+ $this->updateHostFromUri();
+ }
+
+ // If we got no body, defer initialization of the stream until ServerRequest::getBody()
+ if ('' !== $body && null !== $body) {
+ $this->stream = Stream::create($body);
+ }
+ }
+
+ public function getServerParams(): array
+ {
+ return $this->serverParams;
+ }
+
+ public function getUploadedFiles(): array
+ {
+ return $this->uploadedFiles;
+ }
+
+ public function withUploadedFiles(array $uploadedFiles)
+ {
+ $new = clone $this;
+ $new->uploadedFiles = $uploadedFiles;
+
+ return $new;
+ }
+
+ public function getCookieParams(): array
+ {
+ return $this->cookieParams;
+ }
+
+ public function withCookieParams(array $cookies)
+ {
+ $new = clone $this;
+ $new->cookieParams = $cookies;
+
+ return $new;
+ }
+
+ public function getQueryParams(): array
+ {
+ return $this->queryParams;
+ }
+
+ public function withQueryParams(array $query)
+ {
+ $new = clone $this;
+ $new->queryParams = $query;
+
+ return $new;
+ }
+
+ public function getParsedBody()
+ {
+ return $this->parsedBody;
+ }
+
+ public function withParsedBody($data)
+ {
+ if (!\is_array($data) && !\is_object($data) && null !== $data) {
+ throw new \InvalidArgumentException('First parameter to withParsedBody MUST be object, array or null');
+ }
+
+ $new = clone $this;
+ $new->parsedBody = $data;
+
+ return $new;
+ }
+
+ public function getAttributes(): array
+ {
+ return $this->attributes;
+ }
+
+ public function getAttribute($attribute, $default = null)
+ {
+ if (false === \array_key_exists($attribute, $this->attributes)) {
+ return $default;
+ }
+
+ return $this->attributes[$attribute];
+ }
+
+ public function withAttribute($attribute, $value): self
+ {
+ $new = clone $this;
+ $new->attributes[$attribute] = $value;
+
+ return $new;
+ }
+
+ public function withoutAttribute($attribute): self
+ {
+ if (false === \array_key_exists($attribute, $this->attributes)) {
+ return $this;
+ }
+
+ $new = clone $this;
+ unset($new->attributes[$attribute]);
+
+ return $new;
+ }
+ }
+}
+
+// file: vendor/nyholm/psr7/src/Stream.php
+namespace Nyholm\Psr7 {
+
+ use Psr\Http\Message\StreamInterface;
+
+ /**
+ * @author Michael Dowling and contributors to guzzlehttp/psr7
+ * @author Tobias Nyholm
+ * @author Martijn van der Ven
+ */
+ final class Stream implements StreamInterface
+ {
+ /** @var resource|null A resource reference */
+ private $stream;
+
+ /** @var bool */
+ private $seekable;
+
+ /** @var bool */
+ private $readable;
+
+ /** @var bool */
+ private $writable;
+
+ /** @var array|mixed|void|null */
+ private $uri;
+
+ /** @var int|null */
+ private $size;
+
+ /** @var array Hash of readable and writable stream types */
+ /*private*/ const READ_WRITE_HASH = [
+ 'read' => [
+ 'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true,
+ 'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true,
+ 'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true,
+ 'x+t' => true, 'c+t' => true, 'a+' => true,
+ ],
+ 'write' => [
+ 'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true,
+ 'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true,
+ 'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true,
+ 'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true,
+ ],
+ ];
+
+ private function __construct()
+ {
+ }
+
+ /**
+ * Creates a new PSR-7 stream.
+ *
+ * @param string|resource|StreamInterface $body
+ *
+ * @return StreamInterface
+ *
+ * @throws \InvalidArgumentException
+ */
+ public static function create($body = ''): StreamInterface
+ {
+ if ($body instanceof StreamInterface) {
+ return $body;
+ }
+
+ if (\is_string($body)) {
+ $resource = \fopen('php://temp', 'rw+');
+ \fwrite($resource, $body);
+ $body = $resource;
+ }
+
+ if (\is_resource($body)) {
+ $new = new self();
+ $new->stream = $body;
+ $meta = \stream_get_meta_data($new->stream);
+ $new->seekable = $meta['seekable'] && 0 === \fseek($new->stream, 0, \SEEK_CUR);
+ $new->readable = isset(self::READ_WRITE_HASH['read'][$meta['mode']]);
+ $new->writable = isset(self::READ_WRITE_HASH['write'][$meta['mode']]);
+ $new->uri = $new->getMetadata('uri');
+
+ return $new;
+ }
+
+ throw new \InvalidArgumentException('First argument to Stream::create() must be a string, resource or StreamInterface.');
+ }
+
+ /**
+ * Closes the stream when the destructed.
+ */
+ public function __destruct()
+ {
+ $this->close();
+ }
+
+ public function __toString(): string
+ {
+ try {
+ if ($this->isSeekable()) {
+ $this->seek(0);
+ }
+
+ return $this->getContents();
+ } catch (\Exception $e) {
+ return '';
+ }
+ }
+
+ public function close() /*:void*/
+ {
+ if (isset($this->stream)) {
+ if (\is_resource($this->stream)) {
+ \fclose($this->stream);
+ }
+ $this->detach();
+ }
+ }
+
+ public function detach()
+ {
+ if (!isset($this->stream)) {
+ return null;
+ }
+
+ $result = $this->stream;
+ unset($this->stream);
+ $this->size = $this->uri = null;
+ $this->readable = $this->writable = $this->seekable = false;
+
+ return $result;
+ }
+
+ public function getSize() /*:?int*/
+ {
+ if (null !== $this->size) {
+ return $this->size;
+ }
+
+ if (!isset($this->stream)) {
+ return null;
+ }
+
+ // Clear the stat cache if the stream has a URI
+ if ($this->uri) {
+ \clearstatcache(true, $this->uri);
+ }
+
+ $stats = \fstat($this->stream);
+ if (isset($stats['size'])) {
+ $this->size = $stats['size'];
+
+ return $this->size;
+ }
+
+ return null;
+ }
+
+ public function tell(): int
+ {
+ if (false === $result = \ftell($this->stream)) {
+ throw new \RuntimeException('Unable to determine stream position');
+ }
+
+ return $result;
+ }
+
+ public function eof(): bool
+ {
+ return !$this->stream || \feof($this->stream);
+ }
+
+ public function isSeekable(): bool
+ {
+ return $this->seekable;
+ }
+
+ public function seek($offset, $whence = \SEEK_SET) /*:void*/
+ {
+ if (!$this->seekable) {
+ throw new \RuntimeException('Stream is not seekable');
+ }
+
+ if (-1 === \fseek($this->stream, $offset, $whence)) {
+ throw new \RuntimeException('Unable to seek to stream position ' . $offset . ' with whence ' . \var_export($whence, true));
+ }
+ }
+
+ public function rewind() /*:void*/
+ {
+ $this->seek(0);
+ }
+
+ public function isWritable(): bool
+ {
+ return $this->writable;
+ }
+
+ public function write($string): int
+ {
+ if (!$this->writable) {
+ throw new \RuntimeException('Cannot write to a non-writable stream');
+ }
+
+ // We can't know the size after writing anything
+ $this->size = null;
+
+ if (false === $result = \fwrite($this->stream, $string)) {
+ throw new \RuntimeException('Unable to write to stream');
+ }
+
+ return $result;
+ }
+
+ public function isReadable(): bool
+ {
+ return $this->readable;
+ }
+
+ public function read($length): string
+ {
+ if (!$this->readable) {
+ throw new \RuntimeException('Cannot read from non-readable stream');
+ }
+
+ return \fread($this->stream, $length);
+ }
+
+ public function getContents(): string
+ {
+ if (!isset($this->stream)) {
+ throw new \RuntimeException('Unable to read stream contents');
+ }
+
+ if (false === $contents = \stream_get_contents($this->stream)) {
+ throw new \RuntimeException('Unable to read stream contents');
+ }
+
+ return $contents;
+ }
+
+ public function getMetadata($key = null)
+ {
+ if (!isset($this->stream)) {
+ return $key ? null : [];
+ }
+
+ $meta = \stream_get_meta_data($this->stream);
+
+ if (null === $key) {
+ return $meta;
+ }
+
+ return $meta[$key] ?? null;
+ }
+ }
+}
+
+// file: vendor/nyholm/psr7/src/UploadedFile.php
+namespace Nyholm\Psr7 {
+
+ use Psr\Http\Message\{StreamInterface, UploadedFileInterface};
+
+ /**
+ * @author Michael Dowling and contributors to guzzlehttp/psr7
+ * @author Tobias Nyholm
+ * @author Martijn van der Ven
+ */
+ final class UploadedFile implements UploadedFileInterface
+ {
+ /** @var array */
+ /*private*/ const ERRORS = [
+ \UPLOAD_ERR_OK => 1,
+ \UPLOAD_ERR_INI_SIZE => 1,
+ \UPLOAD_ERR_FORM_SIZE => 1,
+ \UPLOAD_ERR_PARTIAL => 1,
+ \UPLOAD_ERR_NO_FILE => 1,
+ \UPLOAD_ERR_NO_TMP_DIR => 1,
+ \UPLOAD_ERR_CANT_WRITE => 1,
+ \UPLOAD_ERR_EXTENSION => 1,
+ ];
+
+ /** @var string */
+ private $clientFilename;
+
+ /** @var string */
+ private $clientMediaType;
+
+ /** @var int */
+ private $error;
+
+ /** @var string|null */
+ private $file;
+
+ /** @var bool */
+ private $moved = false;
+
+ /** @var int */
+ private $size;
+
+ /** @var StreamInterface|null */
+ private $stream;
+
+ /**
+ * @param StreamInterface|string|resource $streamOrFile
+ * @param int $size
+ * @param int $errorStatus
+ * @param string|null $clientFilename
+ * @param string|null $clientMediaType
+ */
+ public function __construct($streamOrFile, $size, $errorStatus, $clientFilename = null, $clientMediaType = null)
+ {
+ if (false === \is_int($errorStatus) || !isset(self::ERRORS[$errorStatus])) {
+ throw new \InvalidArgumentException('Upload file error status must be an integer value and one of the "UPLOAD_ERR_*" constants.');
+ }
+
+ if (false === \is_int($size)) {
+ throw new \InvalidArgumentException('Upload file size must be an integer');
+ }
+
+ if (null !== $clientFilename && !\is_string($clientFilename)) {
+ throw new \InvalidArgumentException('Upload file client filename must be a string or null');
+ }
+
+ if (null !== $clientMediaType && !\is_string($clientMediaType)) {
+ throw new \InvalidArgumentException('Upload file client media type must be a string or null');
+ }
+
+ $this->error = $errorStatus;
+ $this->size = $size;
+ $this->clientFilename = $clientFilename;
+ $this->clientMediaType = $clientMediaType;
+
+ if (\UPLOAD_ERR_OK === $this->error) {
+ // Depending on the value set file or stream variable.
+ if (\is_string($streamOrFile)) {
+ $this->file = $streamOrFile;
+ } elseif (\is_resource($streamOrFile)) {
+ $this->stream = Stream::create($streamOrFile);
+ } elseif ($streamOrFile instanceof StreamInterface) {
+ $this->stream = $streamOrFile;
+ } else {
+ throw new \InvalidArgumentException('Invalid stream or file provided for UploadedFile');
+ }
+ }
+ }
+
+ /**
+ * @throws \RuntimeException if is moved or not ok
+ */
+ private function validateActive() /*:void*/
+ {
+ if (\UPLOAD_ERR_OK !== $this->error) {
+ throw new \RuntimeException('Cannot retrieve stream due to upload error');
+ }
+
+ if ($this->moved) {
+ throw new \RuntimeException('Cannot retrieve stream after it has already been moved');
+ }
+ }
+
+ public function getStream(): StreamInterface
+ {
+ $this->validateActive();
+
+ if ($this->stream instanceof StreamInterface) {
+ return $this->stream;
+ }
+
+ $resource = \fopen($this->file, 'r');
+
+ return Stream::create($resource);
+ }
+
+ public function moveTo($targetPath) /*:void*/
+ {
+ $this->validateActive();
+
+ if (!\is_string($targetPath) || '' === $targetPath) {
+ throw new \InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string');
+ }
+
+ if (null !== $this->file) {
+ $this->moved = 'cli' === \PHP_SAPI ? \rename($this->file, $targetPath) : \move_uploaded_file($this->file, $targetPath);
+ } else {
+ $stream = $this->getStream();
+ if ($stream->isSeekable()) {
+ $stream->rewind();
+ }
+
+ // Copy the contents of a stream into another stream until end-of-file.
+ $dest = Stream::create(\fopen($targetPath, 'w'));
+ while (!$stream->eof()) {
+ if (!$dest->write($stream->read(1048576))) {
+ break;
+ }
+ }
+
+ $this->moved = true;
+ }
+
+ if (false === $this->moved) {
+ throw new \RuntimeException(\sprintf('Uploaded file could not be moved to %s', $targetPath));
+ }
+ }
+
+ public function getSize(): int
+ {
+ return $this->size;
+ }
+
+ public function getError(): int
+ {
+ return $this->error;
+ }
+
+ public function getClientFilename() /*:?string*/
+ {
+ return $this->clientFilename;
+ }
+
+ public function getClientMediaType() /*:?string*/
+ {
+ return $this->clientMediaType;
+ }
+ }
+}
+
+// file: vendor/nyholm/psr7/src/Uri.php
+namespace Nyholm\Psr7 {
+
+ use Psr\Http\Message\UriInterface;
+
+ /**
+ * PSR-7 URI implementation.
+ *
+ * @author Michael Dowling
+ * @author Tobias Schultze
+ * @author Matthew Weier O'Phinney
+ * @author Tobias Nyholm
+ * @author Martijn van der Ven
+ */
+ final class Uri implements UriInterface
+ {
+ /*private*/ const SCHEMES = ['http' => 80, 'https' => 443];
+
+ /*private*/ const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~';
+
+ /*private*/ const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
+
+ /** @var string Uri scheme. */
+ private $scheme = '';
+
+ /** @var string Uri user info. */
+ private $userInfo = '';
+
+ /** @var string Uri host. */
+ private $host = '';
+
+ /** @var int|null Uri port. */
+ private $port;
+
+ /** @var string Uri path. */
+ private $path = '';
+
+ /** @var string Uri query string. */
+ private $query = '';
+
+ /** @var string Uri fragment. */
+ private $fragment = '';
+
+ public function __construct(string $uri = '')
+ {
+ if ('' !== $uri) {
+ if (false === $parts = \parse_url($uri)) {
+ throw new \InvalidArgumentException("Unable to parse URI: $uri");
+ }
+
+ // Apply parse_url parts to a URI.
+ $this->scheme = isset($parts['scheme']) ? \strtolower($parts['scheme']) : '';
+ $this->userInfo = $parts['user'] ?? '';
+ $this->host = isset($parts['host']) ? \strtolower($parts['host']) : '';
+ $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null;
+ $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : '';
+ $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : '';
+ $this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : '';
+ if (isset($parts['pass'])) {
+ $this->userInfo .= ':' . $parts['pass'];
+ }
+ }
+ }
+
+ public function __toString(): string
+ {
+ return self::createUriString($this->scheme, $this->getAuthority(), $this->path, $this->query, $this->fragment);
+ }
+
+ public function getScheme(): string
+ {
+ return $this->scheme;
+ }
+
+ public function getAuthority(): string
+ {
+ if ('' === $this->host) {
+ return '';
+ }
+
+ $authority = $this->host;
+ if ('' !== $this->userInfo) {
+ $authority = $this->userInfo . '@' . $authority;
+ }
+
+ if (null !== $this->port) {
+ $authority .= ':' . $this->port;
+ }
+
+ return $authority;
+ }
+
+ public function getUserInfo(): string
+ {
+ return $this->userInfo;
+ }
+
+ public function getHost(): string
+ {
+ return $this->host;
+ }
+
+ public function getPort() /*:?int*/
+ {
+ return $this->port;
+ }
+
+ public function getPath(): string
+ {
+ return $this->path;
+ }
+
+ public function getQuery(): string
+ {
+ return $this->query;
+ }
+
+ public function getFragment(): string
+ {
+ return $this->fragment;
+ }
+
+ public function withScheme($scheme): self
+ {
+ if (!\is_string($scheme)) {
+ throw new \InvalidArgumentException('Scheme must be a string');
+ }
+
+ if ($this->scheme === $scheme = \strtolower($scheme)) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->scheme = $scheme;
+ $new->port = $new->filterPort($new->port);
+
+ return $new;
+ }
+
+ public function withUserInfo($user, $password = null): self
+ {
+ $info = $user;
+ if (null !== $password && '' !== $password) {
+ $info .= ':' . $password;
+ }
+
+ if ($this->userInfo === $info) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->userInfo = $info;
+
+ return $new;
+ }
+
+ public function withHost($host): self
+ {
+ if (!\is_string($host)) {
+ throw new \InvalidArgumentException('Host must be a string');
+ }
+
+ if ($this->host === $host = \strtolower($host)) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->host = $host;
+
+ return $new;
+ }
+
+ public function withPort($port): self
+ {
+ if ($this->port === $port = $this->filterPort($port)) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->port = $port;
+
+ return $new;
+ }
+
+ public function withPath($path): self
+ {
+ if ($this->path === $path = $this->filterPath($path)) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->path = $path;
+
+ return $new;
+ }
+
+ public function withQuery($query): self
+ {
+ if ($this->query === $query = $this->filterQueryAndFragment($query)) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->query = $query;
+
+ return $new;
+ }
+
+ public function withFragment($fragment): self
+ {
+ if ($this->fragment === $fragment = $this->filterQueryAndFragment($fragment)) {
+ return $this;
+ }
+
+ $new = clone $this;
+ $new->fragment = $fragment;
+
+ return $new;
+ }
+
+ /**
+ * Create a URI string from its various parts.
+ */
+ private static function createUriString(string $scheme, string $authority, string $path, string $query, string $fragment): string
+ {
+ $uri = '';
+ if ('' !== $scheme) {
+ $uri .= $scheme . ':';
+ }
+
+ if ('' !== $authority) {
+ $uri .= '//' . $authority;
+ }
+
+ if ('' !== $path) {
+ if ('/' !== $path[0]) {
+ if ('' !== $authority) {
+ // If the path is rootless and an authority is present, the path MUST be prefixed by "/"
+ $path = '/' . $path;
+ }
+ } elseif (isset($path[1]) && '/' === $path[1]) {
+ if ('' === $authority) {
+ // If the path is starting with more than one "/" and no authority is present, the
+ // starting slashes MUST be reduced to one.
+ $path = '/' . \ltrim($path, '/');
+ }
+ }
+
+ $uri .= $path;
+ }
+
+ if ('' !== $query) {
+ $uri .= '?' . $query;
+ }
+
+ if ('' !== $fragment) {
+ $uri .= '#' . $fragment;
+ }
+
+ return $uri;
+ }
+
+ /**
+ * Is a given port non-standard for the current scheme?
+ */
+ private static function isNonStandardPort(string $scheme, int $port): bool
+ {
+ return !isset(self::SCHEMES[$scheme]) || $port !== self::SCHEMES[$scheme];
+ }
+
+ private function filterPort($port) /*:?int*/
+ {
+ if (null === $port) {
+ return null;
+ }
+
+ $port = (int) $port;
+ if (0 > $port || 0xffff < $port) {
+ throw new \InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 0 and 65535', $port));
+ }
+
+ return self::isNonStandardPort($this->scheme, $port) ? $port : null;
+ }
+
+ private function filterPath($path): string
+ {
+ if (!\is_string($path)) {
+ throw new \InvalidArgumentException('Path must be a string');
+ }
+
+ return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $path);
+ }
+
+ private function filterQueryAndFragment($str): string
+ {
+ if (!\is_string($str)) {
+ throw new \InvalidArgumentException('Query and fragment must be a string');
+ }
+
+ return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $str);
+ }
+
+ private static function rawurlencodeMatchZero(array $match): string
+ {
+ return \rawurlencode($match[0]);
+ }
+ }
+}
+
+// file: vendor/nyholm/psr7-server/src/ServerRequestCreator.php
+namespace Nyholm\Psr7Server {
+
+ use Psr\Http\Message\ServerRequestFactoryInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Message\StreamFactoryInterface;
+ use Psr\Http\Message\StreamInterface;
+ use Psr\Http\Message\UploadedFileFactoryInterface;
+ use Psr\Http\Message\UploadedFileInterface;
+ use Psr\Http\Message\UriFactoryInterface;
+ use Psr\Http\Message\UriInterface;
+
+ /**
+ * @author Tobias Nyholm
+ * @author Martijn van der Ven
+ */
+ final class ServerRequestCreator implements ServerRequestCreatorInterface
+ {
+ private $serverRequestFactory;
+
+ private $uriFactory;
+
+ private $uploadedFileFactory;
+
+ private $streamFactory;
+
+ public function __construct(
+ ServerRequestFactoryInterface $serverRequestFactory,
+ UriFactoryInterface $uriFactory,
+ UploadedFileFactoryInterface $uploadedFileFactory,
+ StreamFactoryInterface $streamFactory
+ ) {
+ $this->serverRequestFactory = $serverRequestFactory;
+ $this->uriFactory = $uriFactory;
+ $this->uploadedFileFactory = $uploadedFileFactory;
+ $this->streamFactory = $streamFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function fromGlobals(): ServerRequestInterface
+ {
+ $server = $_SERVER;
+ if (false === isset($server['REQUEST_METHOD'])) {
+ $server['REQUEST_METHOD'] = 'GET';
+ }
+
+ $headers = \function_exists('getallheaders') ? getallheaders() : static::getHeadersFromServer($_SERVER);
+
+ return $this->fromArrays($server, $headers, $_COOKIE, $_GET, $_POST, $_FILES, \fopen('php://input', 'r') ?: null);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function fromArrays(array $server, array $headers = [], array $cookie = [], array $get = [], array $post = [], array $files = [], $body = null): ServerRequestInterface
+ {
+ $method = $this->getMethodFromEnv($server);
+ $uri = $this->getUriFromEnvWithHTTP($server);
+ $protocol = isset($server['SERVER_PROTOCOL']) ? \str_replace('HTTP/', '', $server['SERVER_PROTOCOL']) : '1.1';
+
+ $serverRequest = $this->serverRequestFactory->createServerRequest($method, $uri, $server);
+ foreach ($headers as $name => $value) {
+ $serverRequest = $serverRequest->withAddedHeader($name, $value);
+ }
+
+ $serverRequest = $serverRequest
+ ->withProtocolVersion($protocol)
+ ->withCookieParams($cookie)
+ ->withQueryParams($get)
+ ->withParsedBody($post)
+ ->withUploadedFiles($this->normalizeFiles($files));
+
+ if (null === $body) {
+ return $serverRequest;
+ }
+
+ if (\is_resource($body)) {
+ $body = $this->streamFactory->createStreamFromResource($body);
+ } elseif (\is_string($body)) {
+ $body = $this->streamFactory->createStream($body);
+ } elseif (!$body instanceof StreamInterface) {
+ throw new \InvalidArgumentException('The $body parameter to ServerRequestCreator::fromArrays must be string, resource or StreamInterface');
+ }
+
+ return $serverRequest->withBody($body);
+ }
+
+ /**
+ * Implementation from Zend\Diactoros\marshalHeadersFromSapi().
+ */
+ public static function getHeadersFromServer(array $server): array
+ {
+ $headers = [];
+ foreach ($server as $key => $value) {
+ // Apache prefixes environment variables with REDIRECT_
+ // if they are added by rewrite rules
+ if (0 === \strpos($key, 'REDIRECT_')) {
+ $key = \substr($key, 9);
+
+ // We will not overwrite existing variables with the
+ // prefixed versions, though
+ if (\array_key_exists($key, $server)) {
+ continue;
+ }
+ }
+
+ if ($value && 0 === \strpos($key, 'HTTP_')) {
+ $name = \strtr(\strtolower(\substr($key, 5)), '_', '-');
+ $headers[$name] = $value;
+
+ continue;
+ }
+
+ if ($value && 0 === \strpos($key, 'CONTENT_')) {
+ $name = 'content-'.\strtolower(\substr($key, 8));
+ $headers[$name] = $value;
+
+ continue;
+ }
+ }
+
+ return $headers;
+ }
+
+ private function getMethodFromEnv(array $environment): string
+ {
+ if (false === isset($environment['REQUEST_METHOD'])) {
+ throw new \InvalidArgumentException('Cannot determine HTTP method');
+ }
+
+ return $environment['REQUEST_METHOD'];
+ }
+
+ private function getUriFromEnvWithHTTP(array $environment): UriInterface
+ {
+ $uri = $this->createUriFromArray($environment);
+ if (empty($uri->getScheme())) {
+ $uri = $uri->withScheme('http');
+ }
+
+ return $uri;
+ }
+
+ /**
+ * Return an UploadedFile instance array.
+ *
+ * @param array $files A array which respect $_FILES structure
+ *
+ * @return UploadedFileInterface[]
+ *
+ * @throws \InvalidArgumentException for unrecognized values
+ */
+ private function normalizeFiles(array $files): array
+ {
+ $normalized = [];
+
+ foreach ($files as $key => $value) {
+ if ($value instanceof UploadedFileInterface) {
+ $normalized[$key] = $value;
+ } elseif (\is_array($value) && isset($value['tmp_name'])) {
+ $normalized[$key] = $this->createUploadedFileFromSpec($value);
+ } elseif (\is_array($value)) {
+ $normalized[$key] = $this->normalizeFiles($value);
+ } else {
+ throw new \InvalidArgumentException('Invalid value in files specification');
+ }
+ }
+
+ return $normalized;
+ }
+
+ /**
+ * Create and return an UploadedFile instance from a $_FILES specification.
+ *
+ * If the specification represents an array of values, this method will
+ * delegate to normalizeNestedFileSpec() and return that return value.
+ *
+ * @param array $value $_FILES struct
+ *
+ * @return array|UploadedFileInterface
+ */
+ private function createUploadedFileFromSpec(array $value)
+ {
+ if (\is_array($value['tmp_name'])) {
+ return $this->normalizeNestedFileSpec($value);
+ }
+
+ try {
+ $stream = $this->streamFactory->createStreamFromFile($value['tmp_name']);
+ } catch (\RuntimeException $e) {
+ $stream = $this->streamFactory->createStream();
+ }
+
+ return $this->uploadedFileFactory->createUploadedFile(
+ $stream,
+ (int) $value['size'],
+ (int) $value['error'],
+ $value['name'],
+ $value['type']
+ );
+ }
+
+ /**
+ * Normalize an array of file specifications.
+ *
+ * Loops through all nested files and returns a normalized array of
+ * UploadedFileInterface instances.
+ *
+ * @param array $files
+ *
+ * @return UploadedFileInterface[]
+ */
+ private function normalizeNestedFileSpec(array $files = []): array
+ {
+ $normalizedFiles = [];
+
+ foreach (\array_keys($files['tmp_name']) as $key) {
+ $spec = [
+ 'tmp_name' => $files['tmp_name'][$key],
+ 'size' => $files['size'][$key],
+ 'error' => $files['error'][$key],
+ 'name' => $files['name'][$key],
+ 'type' => $files['type'][$key],
+ ];
+ $normalizedFiles[$key] = $this->createUploadedFileFromSpec($spec);
+ }
+
+ return $normalizedFiles;
+ }
+
+ /**
+ * Create a new uri from server variable.
+ *
+ * @param array $server typically $_SERVER or similar structure
+ */
+ private function createUriFromArray(array $server): UriInterface
+ {
+ $uri = $this->uriFactory->createUri('');
+
+ if (isset($server['HTTP_X_FORWARDED_PROTO'])) {
+ $uri = $uri->withScheme($server['HTTP_X_FORWARDED_PROTO']);
+ } else {
+ if (isset($server['REQUEST_SCHEME'])) {
+ $uri = $uri->withScheme($server['REQUEST_SCHEME']);
+ } elseif (isset($server['HTTPS'])) {
+ $uri = $uri->withScheme('on' === $server['HTTPS'] ? 'https' : 'http');
+ }
+
+ if (isset($server['SERVER_PORT'])) {
+ $uri = $uri->withPort($server['SERVER_PORT']);
+ }
+ }
+
+ if (isset($server['HTTP_HOST'])) {
+ if (1 === \preg_match('/^(.+)\:(\d+)$/', $server['HTTP_HOST'], $matches)) {
+ $uri = $uri->withHost($matches[1])->withPort($matches[2]);
+ } else {
+ $uri = $uri->withHost($server['HTTP_HOST']);
+ }
+ } elseif (isset($server['SERVER_NAME'])) {
+ $uri = $uri->withHost($server['SERVER_NAME']);
+ }
+
+ if (isset($server['REQUEST_URI'])) {
+ $uri = $uri->withPath(\current(\explode('?', $server['REQUEST_URI'])));
+ }
+
+ if (isset($server['QUERY_STRING'])) {
+ $uri = $uri->withQuery($server['QUERY_STRING']);
+ }
+
+ return $uri;
+ }
+ }
+}
+
+// file: vendor/nyholm/psr7-server/src/ServerRequestCreatorInterface.php
+namespace Nyholm\Psr7Server {
+
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Message\StreamInterface;
+
+ /**
+ * @author Tobias Nyholm
+ * @author Martijn van der Ven
+ */
+ interface ServerRequestCreatorInterface
+ {
+ /**
+ * Create a new server request from the current environment variables.
+ * Defaults to a GET request to minimise the risk of an \InvalidArgumentException.
+ * Includes the current request headers as supplied by the server through `getallheaders()`.
+ * If `getallheaders()` is unavailable on the current server it will fallback to its own `getHeadersFromServer()` method.
+ * Defaults to php://input for the request body.
+ *
+ * @throws \InvalidArgumentException if no valid method or URI can be determined
+ */
+ public function fromGlobals(): ServerRequestInterface;
+
+ /**
+ * Create a new server request from a set of arrays.
+ *
+ * @param array $server typically $_SERVER or similar structure
+ * @param array $headers typically the output of getallheaders() or similar structure
+ * @param array $cookie typically $_COOKIE or similar structure
+ * @param array $get typically $_GET or similar structure
+ * @param array $post typically $_POST or similar structure
+ * @param array $files typically $_FILES or similar structure
+ * @param StreamInterface|resource|string|null $body Typically stdIn
+ *
+ * @throws \InvalidArgumentException if no valid method or URI can be determined
+ */
+ public function fromArrays(
+ array $server,
+ array $headers = [],
+ array $cookie = [],
+ array $get = [],
+ array $post = [],
+ array $files = [],
+ $body = null
+ ): ServerRequestInterface;
+
+ /**
+ * Get parsed headers from ($_SERVER) array.
+ *
+ * @param array $server typically $_SERVER or similar structure
+ *
+ * @return array
+ */
+ public static function getHeadersFromServer(array $server): array;
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Cache/Cache.php
+namespace Tqdev\PhpCrudApi\Cache {
+
+ interface Cache
+ {
+ public function set(string $key, string $value, int $ttl = 0): bool;
+ public function get(string $key): string;
+ public function clear(): bool;
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Cache/CacheFactory.php
+namespace Tqdev\PhpCrudApi\Cache {
+
+ class CacheFactory
+ {
+ public static function create(string $type, string $prefix, string $config): Cache
+ {
+ switch ($type) {
+ case 'TempFile':
+ $cache = new TempFileCache($prefix, $config);
+ break;
+ case 'Redis':
+ $cache = new RedisCache($prefix, $config);
+ break;
+ case 'Memcache':
+ $cache = new MemcacheCache($prefix, $config);
+ break;
+ case 'Memcached':
+ $cache = new MemcachedCache($prefix, $config);
+ break;
+ default:
+ $cache = new NoCache();
+ }
+ return $cache;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Cache/MemcacheCache.php
+namespace Tqdev\PhpCrudApi\Cache {
+
+ class MemcacheCache implements Cache
+ {
+ protected $prefix;
+ protected $memcache;
+
+ public function __construct(string $prefix, string $config)
+ {
+ $this->prefix = $prefix;
+ if ($config == '') {
+ $address = 'localhost';
+ $port = 11211;
+ } elseif (strpos($config, ':') === false) {
+ $address = $config;
+ $port = 11211;
+ } else {
+ list($address, $port) = explode(':', $config);
+ }
+ $this->memcache = $this->create();
+ $this->memcache->addServer($address, $port);
+ }
+
+ protected function create() /*: \Memcache*/
+ {
+ return new \Memcache();
+ }
+
+ public function set(string $key, string $value, int $ttl = 0): bool
+ {
+ return $this->memcache->set($this->prefix . $key, $value, 0, $ttl);
+ }
+
+ public function get(string $key): string
+ {
+ return $this->memcache->get($this->prefix . $key) ?: '';
+ }
+
+ public function clear(): bool
+ {
+ return $this->memcache->flush();
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Cache/MemcachedCache.php
+namespace Tqdev\PhpCrudApi\Cache {
+
+ class MemcachedCache extends MemcacheCache
+ {
+ protected function create() /*: \Memcached*/
+ {
+ return new \Memcached();
+ }
+
+ public function set(string $key, string $value, int $ttl = 0): bool
+ {
+ return $this->memcache->set($this->prefix . $key, $value, $ttl);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Cache/NoCache.php
+namespace Tqdev\PhpCrudApi\Cache {
+
+ class NoCache implements Cache
+ {
+ public function __construct()
+ {
+ }
+
+ public function set(string $key, string $value, int $ttl = 0): bool
+ {
+ return true;
+ }
+
+ public function get(string $key): string
+ {
+ return '';
+ }
+
+ public function clear(): bool
+ {
+ return true;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Cache/RedisCache.php
+namespace Tqdev\PhpCrudApi\Cache {
+
+ class RedisCache implements Cache
+ {
+ protected $prefix;
+ protected $redis;
+
+ public function __construct(string $prefix, string $config)
+ {
+ $this->prefix = $prefix;
+ if ($config == '') {
+ $config = '127.0.0.1';
+ }
+ $params = explode(':', $config, 6);
+ if (isset($params[3])) {
+ $params[3] = null;
+ }
+ $this->redis = new \Redis();
+ call_user_func_array(array($this->redis, 'pconnect'), $params);
+ }
+
+ public function set(string $key, string $value, int $ttl = 0): bool
+ {
+ return $this->redis->set($this->prefix . $key, $value, $ttl);
+ }
+
+ public function get(string $key): string
+ {
+ return $this->redis->get($this->prefix . $key) ?: '';
+ }
+
+ public function clear(): bool
+ {
+ return $this->redis->flushDb();
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Cache/TempFileCache.php
+namespace Tqdev\PhpCrudApi\Cache {
+
+ class TempFileCache implements Cache
+ {
+ const SUFFIX = 'cache';
+
+ private $path;
+ private $segments;
+
+ public function __construct(string $prefix, string $config)
+ {
+ $this->segments = [];
+ $s = DIRECTORY_SEPARATOR;
+ $ps = PATH_SEPARATOR;
+ if ($config == '') {
+ $this->path = sys_get_temp_dir() . $s . $prefix . self::SUFFIX;
+ } elseif (strpos($config, $ps) === false) {
+ $this->path = $config;
+ } else {
+ list($path, $segments) = explode($ps, $config);
+ $this->path = $path;
+ $this->segments = explode(',', $segments);
+ }
+ if (file_exists($this->path) && is_dir($this->path)) {
+ $this->clean($this->path, array_filter($this->segments), strlen(md5('')), false);
+ }
+ }
+
+ private function getFileName(string $key): string
+ {
+ $s = DIRECTORY_SEPARATOR;
+ $md5 = md5($key);
+ $filename = rtrim($this->path, $s) . $s;
+ $i = 0;
+ foreach ($this->segments as $segment) {
+ $filename .= substr($md5, $i, $segment) . $s;
+ $i += $segment;
+ }
+ $filename .= substr($md5, $i);
+ return $filename;
+ }
+
+ public function set(string $key, string $value, int $ttl = 0): bool
+ {
+ $filename = $this->getFileName($key);
+ $dirname = dirname($filename);
+ if (!file_exists($dirname)) {
+ if (!mkdir($dirname, 0755, true)) {
+ return false;
+ }
+ }
+ $string = $ttl . '|' . $value;
+ return $this->filePutContents($filename, $string) !== false;
+ }
+
+ private function filePutContents($filename, $string)
+ {
+ return file_put_contents($filename, $string, LOCK_EX);
+ }
+
+ private function fileGetContents($filename)
+ {
+ $file = fopen($filename, 'rb');
+ if ($file === false) {
+ return false;
+ }
+ $lock = flock($file, LOCK_SH);
+ if (!$lock) {
+ fclose($file);
+ return false;
+ }
+ $string = '';
+ while (!feof($file)) {
+ $string .= fread($file, 8192);
+ }
+ flock($file, LOCK_UN);
+ fclose($file);
+ return $string;
+ }
+
+ private function getString($filename): string
+ {
+ $data = $this->fileGetContents($filename);
+ if ($data === false) {
+ return '';
+ }
+ if (strpos($data, '|') === false) {
+ return '';
+ }
+ list($ttl, $string) = explode('|', $data, 2);
+ if ($ttl > 0 && time() - filemtime($filename) > $ttl) {
+ return '';
+ }
+ return $string;
+ }
+
+ public function get(string $key): string
+ {
+ $filename = $this->getFileName($key);
+ if (!file_exists($filename)) {
+ return '';
+ }
+ $string = $this->getString($filename);
+ if ($string == null) {
+ return '';
+ }
+ return $string;
+ }
+
+ private function clean(string $path, array $segments, int $len, bool $all) /*: void*/
+ {
+ $entries = scandir($path);
+ foreach ($entries as $entry) {
+ if ($entry === '.' || $entry === '..') {
+ continue;
+ }
+ $filename = $path . DIRECTORY_SEPARATOR . $entry;
+ if (count($segments) == 0) {
+ if (strlen($entry) != $len) {
+ continue;
+ }
+ if (file_exists($filename) && is_file($filename)) {
+ if ($all || $this->getString($filename) == null) {
+ @unlink($filename);
+ }
+ }
+ } else {
+ if (strlen($entry) != $segments[0]) {
+ continue;
+ }
+ if (file_exists($filename) && is_dir($filename)) {
+ $this->clean($filename, array_slice($segments, 1), $len - $segments[0], $all);
+ @rmdir($filename);
+ }
+ }
+ }
+ }
+
+ public function clear(): bool
+ {
+ if (!file_exists($this->path) || !is_dir($this->path)) {
+ return false;
+ }
+ $this->clean($this->path, array_filter($this->segments), strlen(md5('')), true);
+ return true;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedColumn.php
+namespace Tqdev\PhpCrudApi\Column\Reflection {
+
+ use Tqdev\PhpCrudApi\Database\GenericReflection;
+
+ class ReflectedColumn implements \JsonSerializable
+ {
+ const DEFAULT_LENGTH = 255;
+ const DEFAULT_PRECISION = 19;
+ const DEFAULT_SCALE = 4;
+
+ private $name;
+ private $type;
+ private $length;
+ private $precision;
+ private $scale;
+ private $nullable;
+ private $pk;
+ private $fk;
+
+ public function __construct(string $name, string $type, int $length, int $precision, int $scale, bool $nullable, bool $pk, string $fk)
+ {
+ $this->name = $name;
+ $this->type = $type;
+ $this->length = $length;
+ $this->precision = $precision;
+ $this->scale = $scale;
+ $this->nullable = $nullable;
+ $this->pk = $pk;
+ $this->fk = $fk;
+ $this->sanitize();
+ }
+
+ private static function parseColumnType(string $columnType, int &$length, int &$precision, int &$scale) /*: void*/
+ {
+ if (!$columnType) {
+ return;
+ }
+ $pos = strpos($columnType, '(');
+ if ($pos) {
+ $dataSize = rtrim(substr($columnType, $pos + 1), ')');
+ if ($length) {
+ $length = (int) $dataSize;
+ } else {
+ $pos = strpos($dataSize, ',');
+ if ($pos) {
+ $precision = (int) substr($dataSize, 0, $pos);
+ $scale = (int) substr($dataSize, $pos + 1);
+ } else {
+ $precision = (int) $dataSize;
+ $scale = 0;
+ }
+ }
+ }
+ }
+
+ private static function getDataSize(int $length, int $precision, int $scale): string
+ {
+ $dataSize = '';
+ if ($length) {
+ $dataSize = $length;
+ } elseif ($precision) {
+ if ($scale) {
+ $dataSize = $precision . ',' . $scale;
+ } else {
+ $dataSize = $precision;
+ }
+ }
+ return $dataSize;
+ }
+
+ public static function fromReflection(GenericReflection $reflection, array $columnResult): ReflectedColumn
+ {
+ $name = $columnResult['COLUMN_NAME'];
+ $dataType = $columnResult['DATA_TYPE'];
+ $length = (int) $columnResult['CHARACTER_MAXIMUM_LENGTH'];
+ $precision = (int) $columnResult['NUMERIC_PRECISION'];
+ $scale = (int) $columnResult['NUMERIC_SCALE'];
+ $columnType = $columnResult['COLUMN_TYPE'];
+ self::parseColumnType($columnType, $length, $precision, $scale);
+ $dataSize = self::getDataSize($length, $precision, $scale);
+ $type = $reflection->toJdbcType($dataType, $dataSize);
+ $nullable = in_array(strtoupper($columnResult['IS_NULLABLE']), ['TRUE', 'YES', 'T', 'Y', '1']);
+ $pk = false;
+ $fk = '';
+ return new ReflectedColumn($name, $type, $length, $precision, $scale, $nullable, $pk, $fk);
+ }
+
+ public static function fromJson(/* object */$json): ReflectedColumn
+ {
+ $name = $json->name;
+ $type = $json->type;
+ $length = isset($json->length) ? (int) $json->length : 0;
+ $precision = isset($json->precision) ? (int) $json->precision : 0;
+ $scale = isset($json->scale) ? (int) $json->scale : 0;
+ $nullable = isset($json->nullable) ? (bool) $json->nullable : false;
+ $pk = isset($json->pk) ? (bool) $json->pk : false;
+ $fk = isset($json->fk) ? $json->fk : '';
+ return new ReflectedColumn($name, $type, $length, $precision, $scale, $nullable, $pk, $fk);
+ }
+
+ private function sanitize()
+ {
+ $this->length = $this->hasLength() ? $this->getLength() : 0;
+ $this->precision = $this->hasPrecision() ? $this->getPrecision() : 0;
+ $this->scale = $this->hasScale() ? $this->getScale() : 0;
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function getNullable(): bool
+ {
+ return $this->nullable;
+ }
+
+ public function getType(): string
+ {
+ return $this->type;
+ }
+
+ public function getLength(): int
+ {
+ return $this->length ?: self::DEFAULT_LENGTH;
+ }
+
+ public function getPrecision(): int
+ {
+ return $this->precision ?: self::DEFAULT_PRECISION;
+ }
+
+ public function getScale(): int
+ {
+ return $this->scale ?: self::DEFAULT_SCALE;
+ }
+
+ public function hasLength(): bool
+ {
+ return in_array($this->type, ['varchar', 'varbinary']);
+ }
+
+ public function hasPrecision(): bool
+ {
+ return $this->type == 'decimal';
+ }
+
+ public function hasScale(): bool
+ {
+ return $this->type == 'decimal';
+ }
+
+ public function isBinary(): bool
+ {
+ return in_array($this->type, ['blob', 'varbinary']);
+ }
+
+ public function isBoolean(): bool
+ {
+ return $this->type == 'boolean';
+ }
+
+ public function isGeometry(): bool
+ {
+ return $this->type == 'geometry';
+ }
+
+ public function isInteger(): bool
+ {
+ return in_array($this->type, ['integer', 'bigint', 'smallint', 'tinyint']);
+ }
+
+ public function setPk($value) /*: void*/
+ {
+ $this->pk = $value;
+ }
+
+ public function getPk(): bool
+ {
+ return $this->pk;
+ }
+
+ public function setFk($value) /*: void*/
+ {
+ $this->fk = $value;
+ }
+
+ public function getFk(): string
+ {
+ return $this->fk;
+ }
+
+ public function serialize()
+ {
+ return [
+ 'name' => $this->name,
+ 'type' => $this->type,
+ 'length' => $this->length,
+ 'precision' => $this->precision,
+ 'scale' => $this->scale,
+ 'nullable' => $this->nullable,
+ 'pk' => $this->pk,
+ 'fk' => $this->fk,
+ ];
+ }
+
+ public function jsonSerialize()
+ {
+ return array_filter($this->serialize());
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedDatabase.php
+namespace Tqdev\PhpCrudApi\Column\Reflection {
+
+ use Tqdev\PhpCrudApi\Database\GenericReflection;
+
+ class ReflectedDatabase implements \JsonSerializable
+ {
+ private $tableTypes;
+
+ public function __construct(array $tableTypes)
+ {
+ $this->tableTypes = $tableTypes;
+ }
+
+ public static function fromReflection(GenericReflection $reflection): ReflectedDatabase
+ {
+ $tableTypes = [];
+ foreach ($reflection->getTables() as $table) {
+ $tableName = $table['TABLE_NAME'];
+ $tableType = $table['TABLE_TYPE'];
+ if (in_array($tableName, $reflection->getIgnoredTables())) {
+ continue;
+ }
+ $tableTypes[$tableName] = $tableType;
+ }
+ return new ReflectedDatabase($tableTypes);
+ }
+
+ public static function fromJson(/* object */$json): ReflectedDatabase
+ {
+ $tableTypes = (array) $json->tables;
+ return new ReflectedDatabase($tableTypes);
+ }
+
+ public function hasTable(string $tableName): bool
+ {
+ return isset($this->tableTypes[$tableName]);
+ }
+
+ public function getType(string $tableName): string
+ {
+ return isset($this->tableTypes[$tableName]) ? $this->tableTypes[$tableName] : '';
+ }
+
+ public function getTableNames(): array
+ {
+ return array_keys($this->tableTypes);
+ }
+
+ public function removeTable(string $tableName): bool
+ {
+ if (!isset($this->tableTypes[$tableName])) {
+ return false;
+ }
+ unset($this->tableTypes[$tableName]);
+ return true;
+ }
+
+ public function serialize()
+ {
+ return [
+ 'tables' => $this->tableTypes,
+ ];
+ }
+
+ public function jsonSerialize()
+ {
+ return $this->serialize();
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedTable.php
+namespace Tqdev\PhpCrudApi\Column\Reflection {
+
+ use Tqdev\PhpCrudApi\Database\GenericReflection;
+
+ class ReflectedTable implements \JsonSerializable
+ {
+ private $name;
+ private $type;
+ private $columns;
+ private $pk;
+ private $fks;
+
+ public function __construct(string $name, string $type, array $columns)
+ {
+ $this->name = $name;
+ $this->type = $type;
+ // set columns
+ $this->columns = [];
+ foreach ($columns as $column) {
+ $columnName = $column->getName();
+ $this->columns[$columnName] = $column;
+ }
+ // set primary key
+ $this->pk = null;
+ foreach ($columns as $column) {
+ if ($column->getPk() == true) {
+ $this->pk = $column;
+ }
+ }
+ // set foreign keys
+ $this->fks = [];
+ foreach ($columns as $column) {
+ $columnName = $column->getName();
+ $referencedTableName = $column->getFk();
+ if ($referencedTableName != '') {
+ $this->fks[$columnName] = $referencedTableName;
+ }
+ }
+ }
+
+ public static function fromReflection(GenericReflection $reflection, string $name, string $type): ReflectedTable
+ {
+ // set columns
+ $columns = [];
+ foreach ($reflection->getTableColumns($name, $type) as $tableColumn) {
+ $column = ReflectedColumn::fromReflection($reflection, $tableColumn);
+ $columns[$column->getName()] = $column;
+ }
+ // set primary key
+ $columnName = false;
+ if ($type == 'view') {
+ $columnName = 'id';
+ } else {
+ $columnNames = $reflection->getTablePrimaryKeys($name);
+ if (count($columnNames) == 1) {
+ $columnName = $columnNames[0];
+ }
+ }
+ if ($columnName && isset($columns[$columnName])) {
+ $pk = $columns[$columnName];
+ $pk->setPk(true);
+ }
+ // set foreign keys
+ if ($type == 'view') {
+ $tables = $reflection->getTables();
+ foreach ($columns as $columnName => $column) {
+ if (substr($columnName, -3) == '_id') {
+ foreach ($tables as $table) {
+ $tableName = $table['TABLE_NAME'];
+ $suffix = $tableName . '_id';
+ if (substr($columnName, -1 * strlen($suffix)) == $suffix) {
+ $column->setFk($tableName);
+ }
+ }
+ }
+ }
+ } else {
+ $fks = $reflection->getTableForeignKeys($name);
+ foreach ($fks as $columnName => $table) {
+ $columns[$columnName]->setFk($table);
+ }
+ }
+ return new ReflectedTable($name, $type, array_values($columns));
+ }
+
+ public static function fromJson( /* object */$json): ReflectedTable
+ {
+ $name = $json->name;
+ $type = isset($json->type) ? $json->type : 'table';
+ $columns = [];
+ if (isset($json->columns) && is_array($json->columns)) {
+ foreach ($json->columns as $column) {
+ $columns[] = ReflectedColumn::fromJson($column);
+ }
+ }
+ return new ReflectedTable($name, $type, $columns);
+ }
+
+ public function hasColumn(string $columnName): bool
+ {
+ return isset($this->columns[$columnName]);
+ }
+
+ public function hasPk(): bool
+ {
+ return $this->pk != null;
+ }
+
+ public function getPk() /*: ?ReflectedColumn */
+ {
+ return $this->pk;
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function getType(): string
+ {
+ return $this->type;
+ }
+
+ public function getColumnNames(): array
+ {
+ return array_keys($this->columns);
+ }
+
+ public function getColumn($columnName): ReflectedColumn
+ {
+ return $this->columns[$columnName];
+ }
+
+ public function getFksTo(string $tableName): array
+ {
+ $columns = array();
+ foreach ($this->fks as $columnName => $referencedTableName) {
+ if ($tableName == $referencedTableName && !is_null($this->columns[$columnName])) {
+ $columns[] = $this->columns[$columnName];
+ }
+ }
+ return $columns;
+ }
+
+ public function removeColumn(string $columnName): bool
+ {
+ if (!isset($this->columns[$columnName])) {
+ return false;
+ }
+ unset($this->columns[$columnName]);
+ return true;
+ }
+
+ public function serialize()
+ {
+ return [
+ 'name' => $this->name,
+ 'type' => $this->type,
+ 'columns' => array_values($this->columns),
+ ];
+ }
+
+ public function jsonSerialize()
+ {
+ return $this->serialize();
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Column/DefinitionService.php
+namespace Tqdev\PhpCrudApi\Column {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+ use Tqdev\PhpCrudApi\Database\GenericDB;
+
+ class DefinitionService
+ {
+ private $db;
+ private $reflection;
+
+ public function __construct(GenericDB $db, ReflectionService $reflection)
+ {
+ $this->db = $db;
+ $this->reflection = $reflection;
+ }
+
+ public function updateTable(string $tableName, /* object */ $changes): bool
+ {
+ $table = $this->reflection->getTable($tableName);
+ $newTable = ReflectedTable::fromJson((object) array_merge((array) $table->jsonSerialize(), (array) $changes));
+ if ($table->getName() != $newTable->getName()) {
+ if (!$this->db->definition()->renameTable($table->getName(), $newTable->getName())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public function updateColumn(string $tableName, string $columnName, /* object */ $changes): bool
+ {
+ $table = $this->reflection->getTable($tableName);
+ $column = $table->getColumn($columnName);
+
+ // remove constraints on other column
+ $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes));
+ if ($newColumn->getPk() != $column->getPk() && $table->hasPk()) {
+ $oldColumn = $table->getPk();
+ if ($oldColumn->getName() != $columnName) {
+ $oldColumn->setPk(false);
+ if (!$this->db->definition()->removeColumnPrimaryKey($table->getName(), $oldColumn->getName(), $oldColumn)) {
+ return false;
+ }
+ }
+ }
+
+ // remove constraints
+ $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), ['pk' => false, 'fk' => false]));
+ if ($newColumn->getPk() != $column->getPk() && !$newColumn->getPk()) {
+ if (!$this->db->definition()->removeColumnPrimaryKey($table->getName(), $column->getName(), $newColumn)) {
+ return false;
+ }
+ }
+ if ($newColumn->getFk() != $column->getFk() && !$newColumn->getFk()) {
+ if (!$this->db->definition()->removeColumnForeignKey($table->getName(), $column->getName(), $newColumn)) {
+ return false;
+ }
+ }
+
+ // name and type
+ $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes));
+ $newColumn->setPk(false);
+ $newColumn->setFk('');
+ if ($newColumn->getName() != $column->getName()) {
+ if (!$this->db->definition()->renameColumn($table->getName(), $column->getName(), $newColumn)) {
+ return false;
+ }
+ }
+ if (
+ $newColumn->getType() != $column->getType() ||
+ $newColumn->getLength() != $column->getLength() ||
+ $newColumn->getPrecision() != $column->getPrecision() ||
+ $newColumn->getScale() != $column->getScale()
+ ) {
+ if (!$this->db->definition()->retypeColumn($table->getName(), $newColumn->getName(), $newColumn)) {
+ return false;
+ }
+ }
+ if ($newColumn->getNullable() != $column->getNullable()) {
+ if (!$this->db->definition()->setColumnNullable($table->getName(), $newColumn->getName(), $newColumn)) {
+ return false;
+ }
+ }
+
+ // add constraints
+ $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes));
+ if ($newColumn->getFk()) {
+ if (!$this->db->definition()->addColumnForeignKey($table->getName(), $newColumn->getName(), $newColumn)) {
+ return false;
+ }
+ }
+ if ($newColumn->getPk()) {
+ if (!$this->db->definition()->addColumnPrimaryKey($table->getName(), $newColumn->getName(), $newColumn)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public function addTable(/* object */$definition)
+ {
+ $newTable = ReflectedTable::fromJson($definition);
+ if (!$this->db->definition()->addTable($newTable)) {
+ return false;
+ }
+ return true;
+ }
+
+ public function addColumn(string $tableName, /* object */ $definition)
+ {
+ $newColumn = ReflectedColumn::fromJson($definition);
+ if (!$this->db->definition()->addColumn($tableName, $newColumn)) {
+ return false;
+ }
+ if ($newColumn->getFk()) {
+ if (!$this->db->definition()->addColumnForeignKey($tableName, $newColumn->getName(), $newColumn)) {
+ return false;
+ }
+ }
+ if ($newColumn->getPk()) {
+ if (!$this->db->definition()->addColumnPrimaryKey($tableName, $newColumn->getName(), $newColumn)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public function removeTable(string $tableName)
+ {
+ if (!$this->db->definition()->removeTable($tableName)) {
+ return false;
+ }
+ return true;
+ }
+
+ public function removeColumn(string $tableName, string $columnName)
+ {
+ $table = $this->reflection->getTable($tableName);
+ $newColumn = $table->getColumn($columnName);
+ if ($newColumn->getPk()) {
+ $newColumn->setPk(false);
+ if (!$this->db->definition()->removeColumnPrimaryKey($table->getName(), $newColumn->getName(), $newColumn)) {
+ return false;
+ }
+ }
+ if ($newColumn->getFk()) {
+ $newColumn->setFk("");
+ if (!$this->db->definition()->removeColumnForeignKey($tableName, $columnName, $newColumn)) {
+ return false;
+ }
+ }
+ if (!$this->db->definition()->removeColumn($tableName, $columnName)) {
+ return false;
+ }
+ return true;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Column/ReflectionService.php
+namespace Tqdev\PhpCrudApi\Column {
+
+ use Tqdev\PhpCrudApi\Cache\Cache;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedDatabase;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+ use Tqdev\PhpCrudApi\Database\GenericDB;
+
+ class ReflectionService
+ {
+ private $db;
+ private $cache;
+ private $ttl;
+ private $database;
+ private $tables;
+
+ public function __construct(GenericDB $db, Cache $cache, int $ttl)
+ {
+ $this->db = $db;
+ $this->cache = $cache;
+ $this->ttl = $ttl;
+ $this->database = null;
+ $this->tables = [];
+ }
+
+ private function database(): ReflectedDatabase
+ {
+ if ($this->database) {
+ return $this->database;
+ }
+ $this->database = $this->loadDatabase(true);
+ return $this->database;
+ }
+
+ private function loadDatabase(bool $useCache): ReflectedDatabase
+ {
+ $key = sprintf('%s-ReflectedDatabase', $this->db->getCacheKey());
+ $data = $useCache ? $this->cache->get($key) : '';
+ if ($data != '') {
+ $database = ReflectedDatabase::fromJson(json_decode(gzuncompress($data)));
+ } else {
+ $database = ReflectedDatabase::fromReflection($this->db->reflection());
+ $data = gzcompress(json_encode($database, JSON_UNESCAPED_UNICODE));
+ $this->cache->set($key, $data, $this->ttl);
+ }
+ return $database;
+ }
+
+ private function loadTable(string $tableName, bool $useCache): ReflectedTable
+ {
+ $key = sprintf('%s-ReflectedTable(%s)', $this->db->getCacheKey(), $tableName);
+ $data = $useCache ? $this->cache->get($key) : '';
+ if ($data != '') {
+ $table = ReflectedTable::fromJson(json_decode(gzuncompress($data)));
+ } else {
+ $tableType = $this->database()->getType($tableName);
+ $table = ReflectedTable::fromReflection($this->db->reflection(), $tableName, $tableType);
+ $data = gzcompress(json_encode($table, JSON_UNESCAPED_UNICODE));
+ $this->cache->set($key, $data, $this->ttl);
+ }
+ return $table;
+ }
+
+ public function refreshTables()
+ {
+ $this->database = $this->loadDatabase(false);
+ }
+
+ public function refreshTable(string $tableName)
+ {
+ $this->tables[$tableName] = $this->loadTable($tableName, false);
+ }
+
+ public function hasTable(string $tableName): bool
+ {
+ return $this->database()->hasTable($tableName);
+ }
+
+ public function getType(string $tableName): string
+ {
+ return $this->database()->getType($tableName);
+ }
+
+ public function getTable(string $tableName): ReflectedTable
+ {
+ if (!isset($this->tables[$tableName])) {
+ $this->tables[$tableName] = $this->loadTable($tableName, true);
+ }
+ return $this->tables[$tableName];
+ }
+
+ public function getTableNames(): array
+ {
+ return $this->database()->getTableNames();
+ }
+
+ public function removeTable(string $tableName): bool
+ {
+ unset($this->tables[$tableName]);
+ return $this->database()->removeTable($tableName);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Controller/CacheController.php
+namespace Tqdev\PhpCrudApi\Controller {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Tqdev\PhpCrudApi\Cache\Cache;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+
+ class CacheController
+ {
+ private $cache;
+ private $responder;
+
+ public function __construct(Router $router, Responder $responder, Cache $cache)
+ {
+ $router->register('GET', '/cache/clear', array($this, 'clear'));
+ $this->cache = $cache;
+ $this->responder = $responder;
+ }
+
+ public function clear(ServerRequestInterface $request): ResponseInterface
+ {
+ return $this->responder->success($this->cache->clear());
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Controller/ColumnController.php
+namespace Tqdev\PhpCrudApi\Controller {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Tqdev\PhpCrudApi\Column\DefinitionService;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class ColumnController
+ {
+ private $responder;
+ private $reflection;
+ private $definition;
+
+ public function __construct(Router $router, Responder $responder, ReflectionService $reflection, DefinitionService $definition)
+ {
+ $router->register('GET', '/columns', array($this, 'getDatabase'));
+ $router->register('GET', '/columns/*', array($this, 'getTable'));
+ $router->register('GET', '/columns/*/*', array($this, 'getColumn'));
+ $router->register('PUT', '/columns/*', array($this, 'updateTable'));
+ $router->register('PUT', '/columns/*/*', array($this, 'updateColumn'));
+ $router->register('POST', '/columns', array($this, 'addTable'));
+ $router->register('POST', '/columns/*', array($this, 'addColumn'));
+ $router->register('DELETE', '/columns/*', array($this, 'removeTable'));
+ $router->register('DELETE', '/columns/*/*', array($this, 'removeColumn'));
+ $this->responder = $responder;
+ $this->reflection = $reflection;
+ $this->definition = $definition;
+ }
+
+ public function getDatabase(ServerRequestInterface $request): ResponseInterface
+ {
+ $tables = [];
+ foreach ($this->reflection->getTableNames() as $table) {
+ $tables[] = $this->reflection->getTable($table);
+ }
+ $database = ['tables' => $tables];
+ return $this->responder->success($database);
+ }
+
+ public function getTable(ServerRequestInterface $request): ResponseInterface
+ {
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ if (!$this->reflection->hasTable($tableName)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
+ }
+ $table = $this->reflection->getTable($tableName);
+ return $this->responder->success($table);
+ }
+
+ public function getColumn(ServerRequestInterface $request): ResponseInterface
+ {
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ $columnName = RequestUtils::getPathSegment($request, 3);
+ if (!$this->reflection->hasTable($tableName)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
+ }
+ $table = $this->reflection->getTable($tableName);
+ if (!$table->hasColumn($columnName)) {
+ return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
+ }
+ $column = $table->getColumn($columnName);
+ return $this->responder->success($column);
+ }
+
+ public function updateTable(ServerRequestInterface $request): ResponseInterface
+ {
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ if (!$this->reflection->hasTable($tableName)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
+ }
+ $success = $this->definition->updateTable($tableName, $request->getParsedBody());
+ if ($success) {
+ $this->reflection->refreshTables();
+ }
+ return $this->responder->success($success);
+ }
+
+ public function updateColumn(ServerRequestInterface $request): ResponseInterface
+ {
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ $columnName = RequestUtils::getPathSegment($request, 3);
+ if (!$this->reflection->hasTable($tableName)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
+ }
+ $table = $this->reflection->getTable($tableName);
+ if (!$table->hasColumn($columnName)) {
+ return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
+ }
+ $success = $this->definition->updateColumn($tableName, $columnName, $request->getParsedBody());
+ if ($success) {
+ $this->reflection->refreshTable($tableName);
+ }
+ return $this->responder->success($success);
+ }
+
+ public function addTable(ServerRequestInterface $request): ResponseInterface
+ {
+ $tableName = $request->getParsedBody()->name;
+ if ($this->reflection->hasTable($tableName)) {
+ return $this->responder->error(ErrorCode::TABLE_ALREADY_EXISTS, $tableName);
+ }
+ $success = $this->definition->addTable($request->getParsedBody());
+ if ($success) {
+ $this->reflection->refreshTables();
+ }
+ return $this->responder->success($success);
+ }
+
+ public function addColumn(ServerRequestInterface $request): ResponseInterface
+ {
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ if (!$this->reflection->hasTable($tableName)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
+ }
+ $columnName = $request->getParsedBody()->name;
+ $table = $this->reflection->getTable($tableName);
+ if ($table->hasColumn($columnName)) {
+ return $this->responder->error(ErrorCode::COLUMN_ALREADY_EXISTS, $columnName);
+ }
+ $success = $this->definition->addColumn($tableName, $request->getParsedBody());
+ if ($success) {
+ $this->reflection->refreshTable($tableName);
+ }
+ return $this->responder->success($success);
+ }
+
+ public function removeTable(ServerRequestInterface $request): ResponseInterface
+ {
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ if (!$this->reflection->hasTable($tableName)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
+ }
+ $success = $this->definition->removeTable($tableName);
+ if ($success) {
+ $this->reflection->refreshTables();
+ }
+ return $this->responder->success($success);
+ }
+
+ public function removeColumn(ServerRequestInterface $request): ResponseInterface
+ {
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ $columnName = RequestUtils::getPathSegment($request, 3);
+ if (!$this->reflection->hasTable($tableName)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
+ }
+ $table = $this->reflection->getTable($tableName);
+ if (!$table->hasColumn($columnName)) {
+ return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
+ }
+ $success = $this->definition->removeColumn($tableName, $columnName);
+ if ($success) {
+ $this->reflection->refreshTable($tableName);
+ }
+ return $this->responder->success($success);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Controller/GeoJsonController.php
+namespace Tqdev\PhpCrudApi\Controller {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Tqdev\PhpCrudApi\GeoJson\GeoJsonService;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class GeoJsonController
+ {
+ private $service;
+ private $responder;
+
+ public function __construct(Router $router, Responder $responder, GeoJsonService $service)
+ {
+ $router->register('GET', '/geojson/*', array($this, '_list'));
+ $router->register('GET', '/geojson/*/*', array($this, 'read'));
+ $this->service = $service;
+ $this->responder = $responder;
+ }
+
+ public function _list(ServerRequestInterface $request): ResponseInterface
+ {
+ $table = RequestUtils::getPathSegment($request, 2);
+ $params = RequestUtils::getParams($request);
+ if (!$this->service->hasTable($table)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
+ }
+ return $this->responder->success($this->service->_list($table, $params));
+ }
+
+ public function read(ServerRequestInterface $request): ResponseInterface
+ {
+ $table = RequestUtils::getPathSegment($request, 2);
+ if (!$this->service->hasTable($table)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
+ }
+ if ($this->service->getType($table) != 'table') {
+ return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
+ }
+ $id = RequestUtils::getPathSegment($request, 3);
+ $params = RequestUtils::getParams($request);
+ if (strpos($id, ',') !== false) {
+ $ids = explode(',', $id);
+ $result = (object) array('type' => 'FeatureCollection', 'features' => array());
+ for ($i = 0; $i < count($ids); $i++) {
+ array_push($result->features, $this->service->read($table, $ids[$i], $params));
+ }
+ return $this->responder->success($result);
+ } else {
+ $response = $this->service->read($table, $id, $params);
+ if ($response === null) {
+ return $this->responder->error(ErrorCode::RECORD_NOT_FOUND, $id);
+ }
+ return $this->responder->success($response);
+ }
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Controller/JsonResponder.php
+namespace Tqdev\PhpCrudApi\Controller {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Tqdev\PhpCrudApi\Record\Document\ErrorDocument;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\ResponseFactory;
+
+ class JsonResponder implements Responder
+ {
+ public function error(int $error, string $argument, $details = null): ResponseInterface
+ {
+ $errorCode = new ErrorCode($error);
+ $status = $errorCode->getStatus();
+ $document = new ErrorDocument($errorCode, $argument, $details);
+ return ResponseFactory::fromObject($status, $document);
+ }
+
+ public function success($result): ResponseInterface
+ {
+ return ResponseFactory::fromObject(ResponseFactory::OK, $result);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Controller/OpenApiController.php
+namespace Tqdev\PhpCrudApi\Controller {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\OpenApi\OpenApiService;
+
+ class OpenApiController
+ {
+ private $openApi;
+ private $responder;
+
+ public function __construct(Router $router, Responder $responder, OpenApiService $openApi)
+ {
+ $router->register('GET', '/openapi', array($this, 'openapi'));
+ $this->openApi = $openApi;
+ $this->responder = $responder;
+ }
+
+ public function openapi(ServerRequestInterface $request): ResponseInterface
+ {
+ return $this->responder->success($this->openApi->get());
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Controller/RecordController.php
+namespace Tqdev\PhpCrudApi\Controller {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\Record\RecordService;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class RecordController
+ {
+ private $service;
+ private $responder;
+
+ public function __construct(Router $router, Responder $responder, RecordService $service)
+ {
+ $router->register('GET', '/records/*', array($this, '_list'));
+ $router->register('POST', '/records/*', array($this, 'create'));
+ $router->register('GET', '/records/*/*', array($this, 'read'));
+ $router->register('PUT', '/records/*/*', array($this, 'update'));
+ $router->register('DELETE', '/records/*/*', array($this, 'delete'));
+ $router->register('PATCH', '/records/*/*', array($this, 'increment'));
+ $this->service = $service;
+ $this->responder = $responder;
+ }
+
+ public function _list(ServerRequestInterface $request): ResponseInterface
+ {
+ $table = RequestUtils::getPathSegment($request, 2);
+ $params = RequestUtils::getParams($request);
+ if (!$this->service->hasTable($table)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
+ }
+ return $this->responder->success($this->service->_list($table, $params));
+ }
+
+ public function read(ServerRequestInterface $request): ResponseInterface
+ {
+ $table = RequestUtils::getPathSegment($request, 2);
+ if (!$this->service->hasTable($table)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
+ }
+ $id = RequestUtils::getPathSegment($request, 3);
+ $params = RequestUtils::getParams($request);
+ if (strpos($id, ',') !== false) {
+ $ids = explode(',', $id);
+ $result = [];
+ for ($i = 0; $i < count($ids); $i++) {
+ array_push($result, $this->service->read($table, $ids[$i], $params));
+ }
+ return $this->responder->success($result);
+ } else {
+ $response = $this->service->read($table, $id, $params);
+ if ($response === null) {
+ return $this->responder->error(ErrorCode::RECORD_NOT_FOUND, $id);
+ }
+ return $this->responder->success($response);
+ }
+ }
+
+ public function create(ServerRequestInterface $request): ResponseInterface
+ {
+ $table = RequestUtils::getPathSegment($request, 2);
+ if (!$this->service->hasTable($table)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
+ }
+ if ($this->service->getType($table) != 'table') {
+ return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
+ }
+ $record = $request->getParsedBody();
+ if ($record === null) {
+ return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, '');
+ }
+ $params = RequestUtils::getParams($request);
+ if (is_array($record)) {
+ $result = array();
+ foreach ($record as $r) {
+ $result[] = $this->service->create($table, $r, $params);
+ }
+ return $this->responder->success($result);
+ } else {
+ return $this->responder->success($this->service->create($table, $record, $params));
+ }
+ }
+
+ public function update(ServerRequestInterface $request): ResponseInterface
+ {
+ $table = RequestUtils::getPathSegment($request, 2);
+ if (!$this->service->hasTable($table)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
+ }
+ if ($this->service->getType($table) != 'table') {
+ return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
+ }
+ $id = RequestUtils::getPathSegment($request, 3);
+ $params = RequestUtils::getParams($request);
+ $record = $request->getParsedBody();
+ if ($record === null) {
+ return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, '');
+ }
+ $ids = explode(',', $id);
+ if (is_array($record)) {
+ if (count($ids) != count($record)) {
+ return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
+ }
+ $result = array();
+ for ($i = 0; $i < count($ids); $i++) {
+ $result[] = $this->service->update($table, $ids[$i], $record[$i], $params);
+ }
+ return $this->responder->success($result);
+ } else {
+ if (count($ids) != 1) {
+ return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
+ }
+ return $this->responder->success($this->service->update($table, $id, $record, $params));
+ }
+ }
+
+ public function delete(ServerRequestInterface $request): ResponseInterface
+ {
+ $table = RequestUtils::getPathSegment($request, 2);
+ if (!$this->service->hasTable($table)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
+ }
+ if ($this->service->getType($table) != 'table') {
+ return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
+ }
+ $id = RequestUtils::getPathSegment($request, 3);
+ $params = RequestUtils::getParams($request);
+ $ids = explode(',', $id);
+ if (count($ids) > 1) {
+ $result = array();
+ for ($i = 0; $i < count($ids); $i++) {
+ $result[] = $this->service->delete($table, $ids[$i], $params);
+ }
+ return $this->responder->success($result);
+ } else {
+ return $this->responder->success($this->service->delete($table, $id, $params));
+ }
+ }
+
+ public function increment(ServerRequestInterface $request): ResponseInterface
+ {
+ $table = RequestUtils::getPathSegment($request, 2);
+ if (!$this->service->hasTable($table)) {
+ return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
+ }
+ if ($this->service->getType($table) != 'table') {
+ return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
+ }
+ $id = RequestUtils::getPathSegment($request, 3);
+ $record = $request->getParsedBody();
+ if ($record === null) {
+ return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, '');
+ }
+ $params = RequestUtils::getParams($request);
+ $ids = explode(',', $id);
+ if (is_array($record)) {
+ if (count($ids) != count($record)) {
+ return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
+ }
+ $result = array();
+ for ($i = 0; $i < count($ids); $i++) {
+ $result[] = $this->service->increment($table, $ids[$i], $record[$i], $params);
+ }
+ return $this->responder->success($result);
+ } else {
+ if (count($ids) != 1) {
+ return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
+ }
+ return $this->responder->success($this->service->increment($table, $id, $record, $params));
+ }
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Controller/Responder.php
+namespace Tqdev\PhpCrudApi\Controller {
+
+ use Psr\Http\Message\ResponseInterface;
+
+ interface Responder
+ {
+ public function error(int $error, string $argument, $details = null): ResponseInterface;
+
+ public function success($result): ResponseInterface;
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Database/ColumnConverter.php
+namespace Tqdev\PhpCrudApi\Database {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
+
+ class ColumnConverter
+ {
+ private $driver;
+
+ public function __construct(string $driver)
+ {
+ $this->driver = $driver;
+ }
+
+ public function convertColumnValue(ReflectedColumn $column): string
+ {
+ if ($column->isBoolean()) {
+ switch ($this->driver) {
+ case 'mysql':
+ return "IFNULL(IF(?,TRUE,FALSE),NULL)";
+ case 'pgsql':
+ return "?";
+ case 'sqlsrv':
+ return "?";
+ }
+ }
+ if ($column->isBinary()) {
+ switch ($this->driver) {
+ case 'mysql':
+ return "FROM_BASE64(?)";
+ case 'pgsql':
+ return "decode(?, 'base64')";
+ case 'sqlsrv':
+ return "CONVERT(XML, ?).value('.','varbinary(max)')";
+ }
+ }
+ if ($column->isGeometry()) {
+ switch ($this->driver) {
+ case 'mysql':
+ case 'pgsql':
+ return "ST_GeomFromText(?)";
+ case 'sqlsrv':
+ return "geometry::STGeomFromText(?,0)";
+ }
+ }
+ return '?';
+ }
+
+ public function convertColumnName(ReflectedColumn $column, $value): string
+ {
+ if ($column->isBinary()) {
+ switch ($this->driver) {
+ case 'mysql':
+ return "TO_BASE64($value) as $value";
+ case 'pgsql':
+ return "encode($value::bytea, 'base64') as $value";
+ case 'sqlsrv':
+ return "CASE WHEN $value IS NULL THEN NULL ELSE (SELECT CAST($value as varbinary(max)) FOR XML PATH(''), BINARY BASE64) END as $value";
+ }
+ }
+ if ($column->isGeometry()) {
+ switch ($this->driver) {
+ case 'mysql':
+ case 'pgsql':
+ return "ST_AsText($value) as $value";
+ case 'sqlsrv':
+ return "REPLACE($value.STAsText(),' (','(') as $value";
+ }
+ }
+ return $value;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Database/ColumnsBuilder.php
+namespace Tqdev\PhpCrudApi\Database {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+
+ class ColumnsBuilder
+ {
+ private $driver;
+ private $converter;
+
+ public function __construct(string $driver)
+ {
+ $this->driver = $driver;
+ $this->converter = new ColumnConverter($driver);
+ }
+
+ public function getOffsetLimit(int $offset, int $limit): string
+ {
+ if ($limit < 0 || $offset < 0) {
+ return '';
+ }
+ switch ($this->driver) {
+ case 'mysql':
+ return " LIMIT $offset, $limit";
+ case 'pgsql':
+ return " LIMIT $limit OFFSET $offset";
+ case 'sqlsrv':
+ return " OFFSET $offset ROWS FETCH NEXT $limit ROWS ONLY";
+ case 'sqlite':
+ return " LIMIT $limit OFFSET $offset";
+ }
+ }
+
+ private function quoteColumnName(ReflectedColumn $column): string
+ {
+ return '"' . $column->getName() . '"';
+ }
+
+ public function getOrderBy(ReflectedTable $table, array $columnOrdering): string
+ {
+ if (count($columnOrdering) == 0) {
+ return '';
+ }
+ $results = array();
+ foreach ($columnOrdering as $i => list($columnName, $ordering)) {
+ $column = $table->getColumn($columnName);
+ $quotedColumnName = $this->quoteColumnName($column);
+ $results[] = $quotedColumnName . ' ' . $ordering;
+ }
+ return ' ORDER BY ' . implode(',', $results);
+ }
+
+ public function getSelect(ReflectedTable $table, array $columnNames): string
+ {
+ $results = array();
+ foreach ($columnNames as $columnName) {
+ $column = $table->getColumn($columnName);
+ $quotedColumnName = $this->quoteColumnName($column);
+ $quotedColumnName = $this->converter->convertColumnName($column, $quotedColumnName);
+ $results[] = $quotedColumnName;
+ }
+ return implode(',', $results);
+ }
+
+ public function getInsert(ReflectedTable $table, array $columnValues): string
+ {
+ $columns = array();
+ $values = array();
+ foreach ($columnValues as $columnName => $columnValue) {
+ $column = $table->getColumn($columnName);
+ $quotedColumnName = $this->quoteColumnName($column);
+ $columns[] = $quotedColumnName;
+ $columnValue = $this->converter->convertColumnValue($column);
+ $values[] = $columnValue;
+ }
+ $columnsSql = '(' . implode(',', $columns) . ')';
+ $valuesSql = '(' . implode(',', $values) . ')';
+ $outputColumn = $this->quoteColumnName($table->getPk());
+ switch ($this->driver) {
+ case 'mysql':
+ return "$columnsSql VALUES $valuesSql";
+ case 'pgsql':
+ return "$columnsSql VALUES $valuesSql RETURNING $outputColumn";
+ case 'sqlsrv':
+ return "$columnsSql OUTPUT INSERTED.$outputColumn VALUES $valuesSql";
+ case 'sqlite':
+ return "$columnsSql VALUES $valuesSql";
+ }
+ }
+
+ public function getUpdate(ReflectedTable $table, array $columnValues): string
+ {
+ $results = array();
+ foreach ($columnValues as $columnName => $columnValue) {
+ $column = $table->getColumn($columnName);
+ $quotedColumnName = $this->quoteColumnName($column);
+ $columnValue = $this->converter->convertColumnValue($column);
+ $results[] = $quotedColumnName . '=' . $columnValue;
+ }
+ return implode(',', $results);
+ }
+
+ public function getIncrement(ReflectedTable $table, array $columnValues): string
+ {
+ $results = array();
+ foreach ($columnValues as $columnName => $columnValue) {
+ if (!is_numeric($columnValue)) {
+ continue;
+ }
+ $column = $table->getColumn($columnName);
+ $quotedColumnName = $this->quoteColumnName($column);
+ $columnValue = $this->converter->convertColumnValue($column);
+ $results[] = $quotedColumnName . '=' . $quotedColumnName . '+' . $columnValue;
+ }
+ return implode(',', $results);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Database/ConditionsBuilder.php
+namespace Tqdev\PhpCrudApi\Database {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
+ use Tqdev\PhpCrudApi\Record\Condition\AndCondition;
+ use Tqdev\PhpCrudApi\Record\Condition\ColumnCondition;
+ use Tqdev\PhpCrudApi\Record\Condition\Condition;
+ use Tqdev\PhpCrudApi\Record\Condition\NoCondition;
+ use Tqdev\PhpCrudApi\Record\Condition\NotCondition;
+ use Tqdev\PhpCrudApi\Record\Condition\OrCondition;
+ use Tqdev\PhpCrudApi\Record\Condition\SpatialCondition;
+
+ class ConditionsBuilder
+ {
+ private $driver;
+
+ public function __construct(string $driver)
+ {
+ $this->driver = $driver;
+ }
+
+ private function getConditionSql(Condition $condition, array &$arguments): string
+ {
+ if ($condition instanceof AndCondition) {
+ return $this->getAndConditionSql($condition, $arguments);
+ }
+ if ($condition instanceof OrCondition) {
+ return $this->getOrConditionSql($condition, $arguments);
+ }
+ if ($condition instanceof NotCondition) {
+ return $this->getNotConditionSql($condition, $arguments);
+ }
+ if ($condition instanceof SpatialCondition) {
+ return $this->getSpatialConditionSql($condition, $arguments);
+ }
+ if ($condition instanceof ColumnCondition) {
+ return $this->getColumnConditionSql($condition, $arguments);
+ }
+ throw new \Exception('Unknown Condition: ' . get_class($condition));
+ }
+
+ private function getAndConditionSql(AndCondition $and, array &$arguments): string
+ {
+ $parts = [];
+ foreach ($and->getConditions() as $condition) {
+ $parts[] = $this->getConditionSql($condition, $arguments);
+ }
+ return '(' . implode(' AND ', $parts) . ')';
+ }
+
+ private function getOrConditionSql(OrCondition $or, array &$arguments): string
+ {
+ $parts = [];
+ foreach ($or->getConditions() as $condition) {
+ $parts[] = $this->getConditionSql($condition, $arguments);
+ }
+ return '(' . implode(' OR ', $parts) . ')';
+ }
+
+ private function getNotConditionSql(NotCondition $not, array &$arguments): string
+ {
+ $condition = $not->getCondition();
+ return '(NOT ' . $this->getConditionSql($condition, $arguments) . ')';
+ }
+
+ private function quoteColumnName(ReflectedColumn $column): string
+ {
+ return '"' . $column->getName() . '"';
+ }
+
+ private function escapeLikeValue(string $value): string
+ {
+ return addcslashes($value, '%_');
+ }
+
+ private function getColumnConditionSql(ColumnCondition $condition, array &$arguments): string
+ {
+ $column = $this->quoteColumnName($condition->getColumn());
+ $operator = $condition->getOperator();
+ $value = $condition->getValue();
+ switch ($operator) {
+ case 'cs':
+ $sql = "$column LIKE ?";
+ $arguments[] = '%' . $this->escapeLikeValue($value) . '%';
+ break;
+ case 'sw':
+ $sql = "$column LIKE ?";
+ $arguments[] = $this->escapeLikeValue($value) . '%';
+ break;
+ case 'ew':
+ $sql = "$column LIKE ?";
+ $arguments[] = '%' . $this->escapeLikeValue($value);
+ break;
+ case 'eq':
+ $sql = "$column = ?";
+ $arguments[] = $value;
+ break;
+ case 'lt':
+ $sql = "$column < ?";
+ $arguments[] = $value;
+ break;
+ case 'le':
+ $sql = "$column <= ?";
+ $arguments[] = $value;
+ break;
+ case 'ge':
+ $sql = "$column >= ?";
+ $arguments[] = $value;
+ break;
+ case 'gt':
+ $sql = "$column > ?";
+ $arguments[] = $value;
+ break;
+ case 'bt':
+ $parts = explode(',', $value, 2);
+ $count = count($parts);
+ if ($count == 2) {
+ $sql = "($column >= ? AND $column <= ?)";
+ $arguments[] = $parts[0];
+ $arguments[] = $parts[1];
+ } else {
+ $sql = "FALSE";
+ }
+ break;
+ case 'in':
+ $parts = explode(',', $value);
+ $count = count($parts);
+ if ($count > 0) {
+ $qmarks = implode(',', str_split(str_repeat('?', $count)));
+ $sql = "$column IN ($qmarks)";
+ for ($i = 0; $i < $count; $i++) {
+ $arguments[] = $parts[$i];
+ }
+ } else {
+ $sql = "FALSE";
+ }
+ break;
+ case 'is':
+ $sql = "$column IS NULL";
+ break;
+ }
+ return $sql;
+ }
+
+ private function getSpatialFunctionName(string $operator): string
+ {
+ switch ($operator) {
+ case 'co':
+ return 'ST_Contains';
+ case 'cr':
+ return 'ST_Crosses';
+ case 'di':
+ return 'ST_Disjoint';
+ case 'eq':
+ return 'ST_Equals';
+ case 'in':
+ return 'ST_Intersects';
+ case 'ov':
+ return 'ST_Overlaps';
+ case 'to':
+ return 'ST_Touches';
+ case 'wi':
+ return 'ST_Within';
+ case 'ic':
+ return 'ST_IsClosed';
+ case 'is':
+ return 'ST_IsSimple';
+ case 'iv':
+ return 'ST_IsValid';
+ }
+ }
+
+ private function hasSpatialArgument(string $operator): bool
+ {
+ return in_array($operator, ['ic', 'is', 'iv']) ? false : true;
+ }
+
+ private function getSpatialFunctionCall(string $functionName, string $column, bool $hasArgument): string
+ {
+ switch ($this->driver) {
+ case 'mysql':
+ case 'pgsql':
+ $argument = $hasArgument ? 'ST_GeomFromText(?)' : '';
+ return "$functionName($column, $argument)=TRUE";
+ case 'sqlsrv':
+ $functionName = str_replace('ST_', 'ST', $functionName);
+ $argument = $hasArgument ? 'geometry::STGeomFromText(?,0)' : '';
+ return "$column.$functionName($argument)=1";
+ case 'sqlite':
+ $argument = $hasArgument ? '?' : '0';
+ return "$functionName($column, $argument)=1";
+ }
+ }
+
+ private function getSpatialConditionSql(ColumnCondition $condition, array &$arguments): string
+ {
+ $column = $this->quoteColumnName($condition->getColumn());
+ $operator = $condition->getOperator();
+ $value = $condition->getValue();
+ $functionName = $this->getSpatialFunctionName($operator);
+ $hasArgument = $this->hasSpatialArgument($operator);
+ $sql = $this->getSpatialFunctionCall($functionName, $column, $hasArgument);
+ if ($hasArgument) {
+ $arguments[] = $value;
+ }
+ return $sql;
+ }
+
+ public function getWhereClause(Condition $condition, array &$arguments): string
+ {
+ if ($condition instanceof NoCondition) {
+ return '';
+ }
+ return ' WHERE ' . $this->getConditionSql($condition, $arguments);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Database/DataConverter.php
+namespace Tqdev\PhpCrudApi\Database {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+
+ class DataConverter
+ {
+ private $driver;
+
+ public function __construct(string $driver)
+ {
+ $this->driver = $driver;
+ }
+
+ private function convertRecordValue($conversion, $value)
+ {
+ $args = explode('|', $conversion);
+ $type = array_shift($args);
+ switch ($type) {
+ case 'boolean':
+ return $value ? true : false;
+ case 'integer':
+ return (int) $value;
+ case 'float':
+ return (float) $value;
+ case 'decimal':
+ return number_format($value, $args[0], '.', '');
+ }
+ return $value;
+ }
+
+ private function getRecordValueConversion(ReflectedColumn $column): string
+ {
+ if (in_array($this->driver, ['mysql', 'sqlsrv', 'sqlite']) && $column->isBoolean()) {
+ return 'boolean';
+ }
+ if (in_array($this->driver, ['sqlsrv', 'sqlite']) && in_array($column->getType(), ['integer', 'bigint'])) {
+ return 'integer';
+ }
+ if (in_array($this->driver, ['sqlite', 'pgsql']) && in_array($column->getType(), ['float', 'double'])) {
+ return 'float';
+ }
+ if (in_array($this->driver, ['sqlite']) && in_array($column->getType(), ['decimal'])) {
+ return 'decimal|' . $column->getScale();
+ }
+ return 'none';
+ }
+
+ public function convertRecords(ReflectedTable $table, array $columnNames, array &$records) /*: void*/
+ {
+ foreach ($columnNames as $columnName) {
+ $column = $table->getColumn($columnName);
+ $conversion = $this->getRecordValueConversion($column);
+ if ($conversion != 'none') {
+ foreach ($records as $i => $record) {
+ $value = $records[$i][$columnName];
+ if ($value === null) {
+ continue;
+ }
+ $records[$i][$columnName] = $this->convertRecordValue($conversion, $value);
+ }
+ }
+ }
+ }
+
+ private function convertInputValue($conversion, $value)
+ {
+ switch ($conversion) {
+ case 'boolean':
+ return $value ? 1 : 0;
+ case 'base64url_to_base64':
+ return str_pad(strtr($value, '-_', '+/'), ceil(strlen($value) / 4) * 4, '=', STR_PAD_RIGHT);
+ }
+ return $value;
+ }
+
+ private function getInputValueConversion(ReflectedColumn $column): string
+ {
+ if ($column->isBoolean()) {
+ return 'boolean';
+ }
+ if ($column->isBinary()) {
+ return 'base64url_to_base64';
+ }
+ return 'none';
+ }
+
+ public function convertColumnValues(ReflectedTable $table, array &$columnValues) /*: void*/
+ {
+ $columnNames = array_keys($columnValues);
+ foreach ($columnNames as $columnName) {
+ $column = $table->getColumn($columnName);
+ $conversion = $this->getInputValueConversion($column);
+ if ($conversion != 'none') {
+ $value = $columnValues[$columnName];
+ if ($value !== null) {
+ $columnValues[$columnName] = $this->convertInputValue($conversion, $value);
+ }
+ }
+ }
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Database/GenericDB.php
+namespace Tqdev\PhpCrudApi\Database {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+ use Tqdev\PhpCrudApi\Middleware\Communication\VariableStore;
+ use Tqdev\PhpCrudApi\Record\Condition\ColumnCondition;
+ use Tqdev\PhpCrudApi\Record\Condition\Condition;
+
+ class GenericDB
+ {
+ private $driver;
+ private $address;
+ private $port;
+ private $database;
+ private $tables;
+ private $username;
+ private $password;
+ private $pdo;
+ private $reflection;
+ private $definition;
+ private $conditions;
+ private $columns;
+ private $converter;
+
+ private function getDsn(): string
+ {
+ switch ($this->driver) {
+ case 'mysql':
+ return "$this->driver:host=$this->address;port=$this->port;dbname=$this->database;charset=utf8mb4";
+ case 'pgsql':
+ return "$this->driver:host=$this->address port=$this->port dbname=$this->database options='--client_encoding=UTF8'";
+ case 'sqlsrv':
+ return "$this->driver:Server=$this->address,$this->port;Database=$this->database";
+ case 'sqlite':
+ return "$this->driver:$this->address";
+ }
+ }
+
+ private function getCommands(): array
+ {
+ switch ($this->driver) {
+ case 'mysql':
+ return [
+ 'SET SESSION sql_warnings=1;',
+ 'SET NAMES utf8mb4;',
+ 'SET SESSION sql_mode = "ANSI,TRADITIONAL";',
+ ];
+ case 'pgsql':
+ return [
+ "SET NAMES 'UTF8';",
+ ];
+ case 'sqlsrv':
+ return [];
+ case 'sqlite':
+ return [
+ 'PRAGMA foreign_keys = on;',
+ ];
+ }
+ }
+
+ private function getOptions(): array
+ {
+ $options = array(
+ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
+ \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
+ );
+ switch ($this->driver) {
+ case 'mysql':
+ return $options + [
+ \PDO::ATTR_EMULATE_PREPARES => false,
+ \PDO::MYSQL_ATTR_FOUND_ROWS => true,
+ \PDO::ATTR_PERSISTENT => true,
+ ];
+ case 'pgsql':
+ return $options + [
+ \PDO::ATTR_EMULATE_PREPARES => false,
+ \PDO::ATTR_PERSISTENT => true,
+ ];
+ case 'sqlsrv':
+ return $options + [
+ \PDO::SQLSRV_ATTR_DIRECT_QUERY => false,
+ \PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE => true,
+ ];
+ case 'sqlite':
+ return $options + [];
+ }
+ }
+
+ private function initPdo(): bool
+ {
+ if ($this->pdo) {
+ $result = $this->pdo->reconstruct($this->getDsn(), $this->username, $this->password, $this->getOptions());
+ } else {
+ $this->pdo = new LazyPdo($this->getDsn(), $this->username, $this->password, $this->getOptions());
+ $result = true;
+ }
+ $commands = $this->getCommands();
+ foreach ($commands as $command) {
+ $this->pdo->addInitCommand($command);
+ }
+ $this->reflection = new GenericReflection($this->pdo, $this->driver, $this->database, $this->tables);
+ $this->definition = new GenericDefinition($this->pdo, $this->driver, $this->database, $this->tables);
+ $this->conditions = new ConditionsBuilder($this->driver);
+ $this->columns = new ColumnsBuilder($this->driver);
+ $this->converter = new DataConverter($this->driver);
+ return $result;
+ }
+
+ public function __construct(string $driver, string $address, int $port, string $database, array $tables, string $username, string $password)
+ {
+ $this->driver = $driver;
+ $this->address = $address;
+ $this->port = $port;
+ $this->database = $database;
+ $this->tables = $tables;
+ $this->username = $username;
+ $this->password = $password;
+ $this->initPdo();
+ }
+
+ public function reconstruct(string $driver, string $address, int $port, string $database, array $tables, string $username, string $password): bool
+ {
+ if ($driver) {
+ $this->driver = $driver;
+ }
+ if ($address) {
+ $this->address = $address;
+ }
+ if ($port) {
+ $this->port = $port;
+ }
+ if ($database) {
+ $this->database = $database;
+ }
+ if ($tables) {
+ $this->tables = $tables;
+ }
+ if ($username) {
+ $this->username = $username;
+ }
+ if ($password) {
+ $this->password = $password;
+ }
+ return $this->initPdo();
+ }
+
+ public function pdo(): LazyPdo
+ {
+ return $this->pdo;
+ }
+
+ public function reflection(): GenericReflection
+ {
+ return $this->reflection;
+ }
+
+ public function definition(): GenericDefinition
+ {
+ return $this->definition;
+ }
+
+ private function addMiddlewareConditions(string $tableName, Condition $condition): Condition
+ {
+ $condition1 = VariableStore::get("authorization.conditions.$tableName");
+ if ($condition1) {
+ $condition = $condition->_and($condition1);
+ }
+ $condition2 = VariableStore::get("multiTenancy.conditions.$tableName");
+ if ($condition2) {
+ $condition = $condition->_and($condition2);
+ }
+ return $condition;
+ }
+
+ public function createSingle(ReflectedTable $table, array $columnValues) /*: ?String*/
+ {
+ $this->converter->convertColumnValues($table, $columnValues);
+ $insertColumns = $this->columns->getInsert($table, $columnValues);
+ $tableName = $table->getName();
+ $pkName = $table->getPk()->getName();
+ $parameters = array_values($columnValues);
+ $sql = 'INSERT INTO "' . $tableName . '" ' . $insertColumns;
+ $stmt = $this->query($sql, $parameters);
+ // return primary key value if specified in the input
+ if (isset($columnValues[$pkName])) {
+ return $columnValues[$pkName];
+ }
+ // work around missing "returning" or "output" in mysql
+ switch ($this->driver) {
+ case 'mysql':
+ $stmt = $this->query('SELECT LAST_INSERT_ID()', []);
+ break;
+ case 'sqlite':
+ $stmt = $this->query('SELECT LAST_INSERT_ROWID()', []);
+ break;
+ }
+ $pkValue = $stmt->fetchColumn(0);
+ if ($this->driver == 'sqlsrv' && $table->getPk()->getType() == 'bigint') {
+ return (int) $pkValue;
+ }
+ if ($this->driver == 'sqlite' && in_array($table->getPk()->getType(), ['integer', 'bigint'])) {
+ return (int) $pkValue;
+ }
+ return $pkValue;
+ }
+
+ public function selectSingle(ReflectedTable $table, array $columnNames, string $id) /*: ?array*/
+ {
+ $selectColumns = $this->columns->getSelect($table, $columnNames);
+ $tableName = $table->getName();
+ $condition = new ColumnCondition($table->getPk(), 'eq', $id);
+ $condition = $this->addMiddlewareConditions($tableName, $condition);
+ $parameters = array();
+ $whereClause = $this->conditions->getWhereClause($condition, $parameters);
+ $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '" ' . $whereClause;
+ $stmt = $this->query($sql, $parameters);
+ $record = $stmt->fetch() ?: null;
+ if ($record === null) {
+ return null;
+ }
+ $records = array($record);
+ $this->converter->convertRecords($table, $columnNames, $records);
+ return $records[0];
+ }
+
+ public function selectMultiple(ReflectedTable $table, array $columnNames, array $ids): array
+ {
+ if (count($ids) == 0) {
+ return [];
+ }
+ $selectColumns = $this->columns->getSelect($table, $columnNames);
+ $tableName = $table->getName();
+ $condition = new ColumnCondition($table->getPk(), 'in', implode(',', $ids));
+ $condition = $this->addMiddlewareConditions($tableName, $condition);
+ $parameters = array();
+ $whereClause = $this->conditions->getWhereClause($condition, $parameters);
+ $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '" ' . $whereClause;
+ $stmt = $this->query($sql, $parameters);
+ $records = $stmt->fetchAll();
+ $this->converter->convertRecords($table, $columnNames, $records);
+ return $records;
+ }
+
+ public function selectCount(ReflectedTable $table, Condition $condition): int
+ {
+ $tableName = $table->getName();
+ $condition = $this->addMiddlewareConditions($tableName, $condition);
+ $parameters = array();
+ $whereClause = $this->conditions->getWhereClause($condition, $parameters);
+ $sql = 'SELECT COUNT(*) FROM "' . $tableName . '"' . $whereClause;
+ $stmt = $this->query($sql, $parameters);
+ return $stmt->fetchColumn(0);
+ }
+
+ public function selectAll(ReflectedTable $table, array $columnNames, Condition $condition, array $columnOrdering, int $offset, int $limit): array
+ {
+ if ($limit == 0) {
+ return array();
+ }
+ $selectColumns = $this->columns->getSelect($table, $columnNames);
+ $tableName = $table->getName();
+ $condition = $this->addMiddlewareConditions($tableName, $condition);
+ $parameters = array();
+ $whereClause = $this->conditions->getWhereClause($condition, $parameters);
+ $orderBy = $this->columns->getOrderBy($table, $columnOrdering);
+ $offsetLimit = $this->columns->getOffsetLimit($offset, $limit);
+ $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '"' . $whereClause . $orderBy . $offsetLimit;
+ $stmt = $this->query($sql, $parameters);
+ $records = $stmt->fetchAll();
+ $this->converter->convertRecords($table, $columnNames, $records);
+ return $records;
+ }
+
+ public function updateSingle(ReflectedTable $table, array $columnValues, string $id)
+ {
+ if (count($columnValues) == 0) {
+ return 0;
+ }
+ $this->converter->convertColumnValues($table, $columnValues);
+ $updateColumns = $this->columns->getUpdate($table, $columnValues);
+ $tableName = $table->getName();
+ $condition = new ColumnCondition($table->getPk(), 'eq', $id);
+ $condition = $this->addMiddlewareConditions($tableName, $condition);
+ $parameters = array_values($columnValues);
+ $whereClause = $this->conditions->getWhereClause($condition, $parameters);
+ $sql = 'UPDATE "' . $tableName . '" SET ' . $updateColumns . $whereClause;
+ $stmt = $this->query($sql, $parameters);
+ return $stmt->rowCount();
+ }
+
+ public function deleteSingle(ReflectedTable $table, string $id)
+ {
+ $tableName = $table->getName();
+ $condition = new ColumnCondition($table->getPk(), 'eq', $id);
+ $condition = $this->addMiddlewareConditions($tableName, $condition);
+ $parameters = array();
+ $whereClause = $this->conditions->getWhereClause($condition, $parameters);
+ $sql = 'DELETE FROM "' . $tableName . '" ' . $whereClause;
+ $stmt = $this->query($sql, $parameters);
+ return $stmt->rowCount();
+ }
+
+ public function incrementSingle(ReflectedTable $table, array $columnValues, string $id)
+ {
+ if (count($columnValues) == 0) {
+ return 0;
+ }
+ $this->converter->convertColumnValues($table, $columnValues);
+ $updateColumns = $this->columns->getIncrement($table, $columnValues);
+ $tableName = $table->getName();
+ $condition = new ColumnCondition($table->getPk(), 'eq', $id);
+ $condition = $this->addMiddlewareConditions($tableName, $condition);
+ $parameters = array_values($columnValues);
+ $whereClause = $this->conditions->getWhereClause($condition, $parameters);
+ $sql = 'UPDATE "' . $tableName . '" SET ' . $updateColumns . $whereClause;
+ $stmt = $this->query($sql, $parameters);
+ return $stmt->rowCount();
+ }
+
+ private function query(string $sql, array $parameters): \PDOStatement
+ {
+ $stmt = $this->pdo->prepare($sql);
+ //echo "- $sql -- " . json_encode($parameters, JSON_UNESCAPED_UNICODE) . "\n";
+ $stmt->execute($parameters);
+ return $stmt;
+ }
+
+ public function getCacheKey(): string
+ {
+ return md5(json_encode([
+ $this->driver,
+ $this->address,
+ $this->port,
+ $this->database,
+ $this->tables,
+ $this->username
+ ]));
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Database/GenericDefinition.php
+namespace Tqdev\PhpCrudApi\Database {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+ use Tqdev\PhpCrudApi\Database\LazyPdo;
+
+ class GenericDefinition
+ {
+ private $pdo;
+ private $driver;
+ private $database;
+ private $typeConverter;
+ private $reflection;
+
+ public function __construct(LazyPdo $pdo, string $driver, string $database, array $tables)
+ {
+ $this->pdo = $pdo;
+ $this->driver = $driver;
+ $this->database = $database;
+ $this->typeConverter = new TypeConverter($driver);
+ $this->reflection = new GenericReflection($pdo, $driver, $database, $tables);
+ }
+
+ private function quote(string $identifier): string
+ {
+ return '"' . str_replace('"', '', $identifier) . '"';
+ }
+
+ public function getColumnType(ReflectedColumn $column, bool $update): string
+ {
+ if ($this->driver == 'pgsql' && !$update && $column->getPk() && $this->canAutoIncrement($column)) {
+ return 'serial';
+ }
+ $type = $this->typeConverter->fromJdbc($column->getType());
+ if ($column->hasPrecision() && $column->hasScale()) {
+ $size = '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
+ } elseif ($column->hasPrecision()) {
+ $size = '(' . $column->getPrecision() . ')';
+ } elseif ($column->hasLength()) {
+ $size = '(' . $column->getLength() . ')';
+ } else {
+ $size = '';
+ }
+ $null = $this->getColumnNullType($column, $update);
+ $auto = $this->getColumnAutoIncrement($column, $update);
+ return $type . $size . $null . $auto;
+ }
+
+ private function getPrimaryKey(string $tableName): string
+ {
+ $pks = $this->reflection->getTablePrimaryKeys($tableName);
+ if (count($pks) == 1) {
+ return $pks[0];
+ }
+ return "";
+ }
+
+ private function canAutoIncrement(ReflectedColumn $column): bool
+ {
+ return in_array($column->getType(), ['integer', 'bigint']);
+ }
+
+ private function getColumnAutoIncrement(ReflectedColumn $column, bool $update): string
+ {
+ if (!$this->canAutoIncrement($column)) {
+ return '';
+ }
+ switch ($this->driver) {
+ case 'mysql':
+ return $column->getPk() ? ' AUTO_INCREMENT' : '';
+ case 'pgsql':
+ case 'sqlsrv':
+ return $column->getPk() ? ' IDENTITY(1,1)' : '';
+ case 'sqlite':
+ return $column->getPk() ? ' AUTOINCREMENT' : '';
+ }
+ }
+
+ private function getColumnNullType(ReflectedColumn $column, bool $update): string
+ {
+ if ($this->driver == 'pgsql' && $update) {
+ return '';
+ }
+ return $column->getNullable() ? ' NULL' : ' NOT NULL';
+ }
+
+ private function getTableRenameSQL(string $tableName, string $newTableName): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($newTableName);
+
+ switch ($this->driver) {
+ case 'mysql':
+ return "RENAME TABLE $p1 TO $p2";
+ case 'pgsql':
+ return "ALTER TABLE $p1 RENAME TO $p2";
+ case 'sqlsrv':
+ return "EXEC sp_rename $p1, $p2";
+ case 'sqlite':
+ return "ALTER TABLE $p1 RENAME TO $p2";
+ }
+ }
+
+ private function getColumnRenameSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($columnName);
+ $p3 = $this->quote($newColumn->getName());
+
+ switch ($this->driver) {
+ case 'mysql':
+ $p4 = $this->getColumnType($newColumn, true);
+ return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
+ case 'pgsql':
+ return "ALTER TABLE $p1 RENAME COLUMN $p2 TO $p3";
+ case 'sqlsrv':
+ $p4 = $this->quote($tableName . '.' . $columnName);
+ return "EXEC sp_rename $p4, $p3, 'COLUMN'";
+ case 'sqlite':
+ return "ALTER TABLE $p1 RENAME COLUMN $p2 TO $p3";
+ }
+ }
+
+ private function getColumnRetypeSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($columnName);
+ $p3 = $this->quote($newColumn->getName());
+ $p4 = $this->getColumnType($newColumn, true);
+
+ switch ($this->driver) {
+ case 'mysql':
+ return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
+ case 'pgsql':
+ return "ALTER TABLE $p1 ALTER COLUMN $p3 TYPE $p4";
+ case 'sqlsrv':
+ return "ALTER TABLE $p1 ALTER COLUMN $p3 $p4";
+ }
+ }
+
+ private function getSetColumnNullableSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($columnName);
+ $p3 = $this->quote($newColumn->getName());
+ $p4 = $this->getColumnType($newColumn, true);
+
+ switch ($this->driver) {
+ case 'mysql':
+ return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
+ case 'pgsql':
+ $p5 = $newColumn->getNullable() ? 'DROP NOT NULL' : 'SET NOT NULL';
+ return "ALTER TABLE $p1 ALTER COLUMN $p2 $p5";
+ case 'sqlsrv':
+ return "ALTER TABLE $p1 ALTER COLUMN $p2 $p4";
+ }
+ }
+
+ private function getSetColumnPkConstraintSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($columnName);
+ $p3 = $this->quote($tableName . '_pkey');
+
+ switch ($this->driver) {
+ case 'mysql':
+ $p4 = $newColumn->getPk() ? "ADD PRIMARY KEY ($p2)" : 'DROP PRIMARY KEY';
+ return "ALTER TABLE $p1 $p4";
+ case 'pgsql':
+ case 'sqlsrv':
+ $p4 = $newColumn->getPk() ? "ADD CONSTRAINT $p3 PRIMARY KEY ($p2)" : "DROP CONSTRAINT $p3";
+ return "ALTER TABLE $p1 $p4";
+ }
+ }
+
+ private function getSetColumnPkSequenceSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($columnName);
+ $p3 = $this->quote($tableName . '_' . $columnName . '_seq');
+
+ switch ($this->driver) {
+ case 'mysql':
+ return "select 1";
+ case 'pgsql':
+ return $newColumn->getPk() ? "CREATE SEQUENCE $p3 OWNED BY $p1.$p2" : "DROP SEQUENCE $p3";
+ case 'sqlsrv':
+ return $newColumn->getPk() ? "CREATE SEQUENCE $p3" : "DROP SEQUENCE $p3";
+ }
+ }
+
+ private function getSetColumnPkSequenceStartSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($columnName);
+
+ switch ($this->driver) {
+ case 'mysql':
+ return "select 1";
+ case 'pgsql':
+ $p3 = $this->pdo->quote($tableName . '_' . $columnName . '_seq');
+ return "SELECT setval($p3, (SELECT max($p2)+1 FROM $p1));";
+ case 'sqlsrv':
+ $p3 = $this->quote($tableName . '_' . $columnName . '_seq');
+ $p4 = $this->pdo->query("SELECT max($p2)+1 FROM $p1")->fetchColumn();
+ return "ALTER SEQUENCE $p3 RESTART WITH $p4";
+ }
+ }
+
+ private function getSetColumnPkDefaultSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($columnName);
+
+ switch ($this->driver) {
+ case 'mysql':
+ $p3 = $this->quote($newColumn->getName());
+ $p4 = $this->getColumnType($newColumn, true);
+ return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
+ case 'pgsql':
+ if ($newColumn->getPk()) {
+ $p3 = $this->pdo->quote($tableName . '_' . $columnName . '_seq');
+ $p4 = "SET DEFAULT nextval($p3)";
+ } else {
+ $p4 = 'DROP DEFAULT';
+ }
+ return "ALTER TABLE $p1 ALTER COLUMN $p2 $p4";
+ case 'sqlsrv':
+ $p3 = $this->quote($tableName . '_' . $columnName . '_seq');
+ $p4 = $this->quote($tableName . '_' . $columnName . '_def');
+ if ($newColumn->getPk()) {
+ return "ALTER TABLE $p1 ADD CONSTRAINT $p4 DEFAULT NEXT VALUE FOR $p3 FOR $p2";
+ } else {
+ return "ALTER TABLE $p1 DROP CONSTRAINT $p4";
+ }
+ }
+ }
+
+ private function getAddColumnFkConstraintSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($columnName);
+ $p3 = $this->quote($tableName . '_' . $columnName . '_fkey');
+ $p4 = $this->quote($newColumn->getFk());
+ $p5 = $this->quote($this->getPrimaryKey($newColumn->getFk()));
+
+ return "ALTER TABLE $p1 ADD CONSTRAINT $p3 FOREIGN KEY ($p2) REFERENCES $p4 ($p5)";
+ }
+
+ private function getRemoveColumnFkConstraintSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($tableName . '_' . $columnName . '_fkey');
+
+ switch ($this->driver) {
+ case 'mysql':
+ return "ALTER TABLE $p1 DROP FOREIGN KEY $p2";
+ case 'pgsql':
+ case 'sqlsrv':
+ return "ALTER TABLE $p1 DROP CONSTRAINT $p2";
+ }
+ }
+
+ private function getAddTableSQL(ReflectedTable $newTable): string
+ {
+ $tableName = $newTable->getName();
+ $p1 = $this->quote($tableName);
+ $fields = [];
+ $constraints = [];
+ foreach ($newTable->getColumnNames() as $columnName) {
+ $pkColumn = $this->getPrimaryKey($tableName);
+ $newColumn = $newTable->getColumn($columnName);
+ $f1 = $this->quote($columnName);
+ $f2 = $this->getColumnType($newColumn, false);
+ $f3 = $this->quote($tableName . '_' . $columnName . '_fkey');
+ $f4 = $this->quote($newColumn->getFk());
+ $f5 = $this->quote($this->getPrimaryKey($newColumn->getFk()));
+ $f6 = $this->quote($tableName . '_' . $pkColumn . '_pkey');
+ if ($this->driver == 'sqlite') {
+ if ($newColumn->getPk()) {
+ $f2 = str_replace('NULL', 'NULL PRIMARY KEY', $f2);
+ }
+ $fields[] = "$f1 $f2";
+ if ($newColumn->getFk()) {
+ $constraints[] = "FOREIGN KEY ($f1) REFERENCES $f4 ($f5)";
+ }
+ } else {
+ $fields[] = "$f1 $f2";
+ if ($newColumn->getPk()) {
+ $constraints[] = "CONSTRAINT $f6 PRIMARY KEY ($f1)";
+ }
+ if ($newColumn->getFk()) {
+ $constraints[] = "CONSTRAINT $f3 FOREIGN KEY ($f1) REFERENCES $f4 ($f5)";
+ }
+ }
+ }
+ $p2 = implode(',', array_merge($fields, $constraints));
+
+ return "CREATE TABLE $p1 ($p2);";
+ }
+
+ private function getAddColumnSQL(string $tableName, ReflectedColumn $newColumn): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($newColumn->getName());
+ $p3 = $this->getColumnType($newColumn, false);
+
+ switch ($this->driver) {
+ case 'mysql':
+ case 'pgsql':
+ return "ALTER TABLE $p1 ADD COLUMN $p2 $p3";
+ case 'sqlsrv':
+ return "ALTER TABLE $p1 ADD $p2 $p3";
+ case 'sqlite':
+ return "ALTER TABLE $p1 ADD COLUMN $p2 $p3";
+ }
+ }
+
+ private function getRemoveTableSQL(string $tableName): string
+ {
+ $p1 = $this->quote($tableName);
+
+ switch ($this->driver) {
+ case 'mysql':
+ case 'pgsql':
+ return "DROP TABLE $p1 CASCADE;";
+ case 'sqlsrv':
+ return "DROP TABLE $p1;";
+ case 'sqlite':
+ return "DROP TABLE $p1;";
+ }
+ }
+
+ private function getRemoveColumnSQL(string $tableName, string $columnName): string
+ {
+ $p1 = $this->quote($tableName);
+ $p2 = $this->quote($columnName);
+
+ switch ($this->driver) {
+ case 'mysql':
+ case 'pgsql':
+ return "ALTER TABLE $p1 DROP COLUMN $p2 CASCADE;";
+ case 'sqlsrv':
+ return "ALTER TABLE $p1 DROP COLUMN $p2;";
+ case 'sqlite':
+ return "ALTER TABLE $p1 DROP COLUMN $p2;";
+ }
+ }
+
+ public function renameTable(string $tableName, string $newTableName)
+ {
+ $sql = $this->getTableRenameSQL($tableName, $newTableName);
+ return $this->query($sql, []);
+ }
+
+ public function renameColumn(string $tableName, string $columnName, ReflectedColumn $newColumn)
+ {
+ $sql = $this->getColumnRenameSQL($tableName, $columnName, $newColumn);
+ return $this->query($sql, []);
+ }
+
+ public function retypeColumn(string $tableName, string $columnName, ReflectedColumn $newColumn)
+ {
+ $sql = $this->getColumnRetypeSQL($tableName, $columnName, $newColumn);
+ return $this->query($sql, []);
+ }
+
+ public function setColumnNullable(string $tableName, string $columnName, ReflectedColumn $newColumn)
+ {
+ $sql = $this->getSetColumnNullableSQL($tableName, $columnName, $newColumn);
+ return $this->query($sql, []);
+ }
+
+ public function addColumnPrimaryKey(string $tableName, string $columnName, ReflectedColumn $newColumn)
+ {
+ $sql = $this->getSetColumnPkConstraintSQL($tableName, $columnName, $newColumn);
+ $this->query($sql, []);
+ if ($this->canAutoIncrement($newColumn)) {
+ $sql = $this->getSetColumnPkSequenceSQL($tableName, $columnName, $newColumn);
+ $this->query($sql, []);
+ $sql = $this->getSetColumnPkSequenceStartSQL($tableName, $columnName, $newColumn);
+ $this->query($sql, []);
+ $sql = $this->getSetColumnPkDefaultSQL($tableName, $columnName, $newColumn);
+ $this->query($sql, []);
+ }
+ return true;
+ }
+
+ public function removeColumnPrimaryKey(string $tableName, string $columnName, ReflectedColumn $newColumn)
+ {
+ if ($this->canAutoIncrement($newColumn)) {
+ $sql = $this->getSetColumnPkDefaultSQL($tableName, $columnName, $newColumn);
+ $this->query($sql, []);
+ $sql = $this->getSetColumnPkSequenceSQL($tableName, $columnName, $newColumn);
+ $this->query($sql, []);
+ }
+ $sql = $this->getSetColumnPkConstraintSQL($tableName, $columnName, $newColumn);
+ $this->query($sql, []);
+ return true;
+ }
+
+ public function addColumnForeignKey(string $tableName, string $columnName, ReflectedColumn $newColumn)
+ {
+ $sql = $this->getAddColumnFkConstraintSQL($tableName, $columnName, $newColumn);
+ return $this->query($sql, []);
+ }
+
+ public function removeColumnForeignKey(string $tableName, string $columnName, ReflectedColumn $newColumn)
+ {
+ $sql = $this->getRemoveColumnFkConstraintSQL($tableName, $columnName, $newColumn);
+ return $this->query($sql, []);
+ }
+
+ public function addTable(ReflectedTable $newTable)
+ {
+ $sql = $this->getAddTableSQL($newTable);
+ return $this->query($sql, []);
+ }
+
+ public function addColumn(string $tableName, ReflectedColumn $newColumn)
+ {
+ $sql = $this->getAddColumnSQL($tableName, $newColumn);
+ return $this->query($sql, []);
+ }
+
+ public function removeTable(string $tableName)
+ {
+ $sql = $this->getRemoveTableSQL($tableName);
+ return $this->query($sql, []);
+ }
+
+ public function removeColumn(string $tableName, string $columnName)
+ {
+ $sql = $this->getRemoveColumnSQL($tableName, $columnName);
+ return $this->query($sql, []);
+ }
+
+ private function query(string $sql, array $arguments): bool
+ {
+ $stmt = $this->pdo->prepare($sql);
+ // echo "- $sql -- " . json_encode($arguments) . "\n";
+ return $stmt->execute($arguments);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Database/GenericReflection.php
+namespace Tqdev\PhpCrudApi\Database {
+
+ use Tqdev\PhpCrudApi\Database\LazyPdo;
+
+ class GenericReflection
+ {
+ private $pdo;
+ private $driver;
+ private $database;
+ private $tables;
+ private $typeConverter;
+
+ public function __construct(LazyPdo $pdo, string $driver, string $database, array $tables)
+ {
+ $this->pdo = $pdo;
+ $this->driver = $driver;
+ $this->database = $database;
+ $this->tables = $tables;
+ $this->typeConverter = new TypeConverter($driver);
+ }
+
+ public function getIgnoredTables(): array
+ {
+ switch ($this->driver) {
+ case 'mysql':
+ return [];
+ case 'pgsql':
+ return ['spatial_ref_sys', 'raster_columns', 'raster_overviews', 'geography_columns', 'geometry_columns'];
+ case 'sqlsrv':
+ return [];
+ case 'sqlite':
+ return [];
+ }
+ }
+
+ private function getTablesSQL(): string
+ {
+ switch ($this->driver) {
+ case 'mysql':
+ return 'SELECT "TABLE_NAME", "TABLE_TYPE" FROM "INFORMATION_SCHEMA"."TABLES" WHERE "TABLE_TYPE" IN (\'BASE TABLE\' , \'VIEW\') AND "TABLE_SCHEMA" = ? ORDER BY BINARY "TABLE_NAME"';
+ case 'pgsql':
+ return 'SELECT c.relname as "TABLE_NAME", c.relkind as "TABLE_TYPE" FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind IN (\'r\', \'v\') AND n.nspname <> \'pg_catalog\' AND n.nspname <> \'information_schema\' AND n.nspname !~ \'^pg_toast\' AND pg_catalog.pg_table_is_visible(c.oid) AND \'\' <> ? ORDER BY "TABLE_NAME";';
+ case 'sqlsrv':
+ return 'SELECT o.name as "TABLE_NAME", o.xtype as "TABLE_TYPE" FROM sysobjects o WHERE o.xtype IN (\'U\', \'V\') ORDER BY "TABLE_NAME"';
+ case 'sqlite':
+ return 'SELECT t.name as "TABLE_NAME", t.type as "TABLE_TYPE" FROM sqlite_master t WHERE t.type IN (\'table\', \'view\') AND \'\' <> ? ORDER BY "TABLE_NAME"';
+ }
+ }
+
+ private function getTableColumnsSQL(): string
+ {
+ switch ($this->driver) {
+ case 'mysql':
+ return 'SELECT "COLUMN_NAME", "IS_NULLABLE", "DATA_TYPE", "CHARACTER_MAXIMUM_LENGTH" as "CHARACTER_MAXIMUM_LENGTH", "NUMERIC_PRECISION", "NUMERIC_SCALE", "COLUMN_TYPE" FROM "INFORMATION_SCHEMA"."COLUMNS" WHERE "TABLE_NAME" = ? AND "TABLE_SCHEMA" = ? ORDER BY "ORDINAL_POSITION"';
+ case 'pgsql':
+ return 'SELECT a.attname AS "COLUMN_NAME", case when a.attnotnull then \'NO\' else \'YES\' end as "IS_NULLABLE", pg_catalog.format_type(a.atttypid, -1) as "DATA_TYPE", case when a.atttypmod < 0 then NULL else a.atttypmod-4 end as "CHARACTER_MAXIMUM_LENGTH", case when a.atttypid != 1700 then NULL else ((a.atttypmod - 4) >> 16) & 65535 end as "NUMERIC_PRECISION", case when a.atttypid != 1700 then NULL else (a.atttypmod - 4) & 65535 end as "NUMERIC_SCALE", \'\' AS "COLUMN_TYPE" FROM pg_attribute a JOIN pg_class pgc ON pgc.oid = a.attrelid WHERE pgc.relname = ? AND \'\' <> ? AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum;';
+ case 'sqlsrv':
+ return 'SELECT c.name AS "COLUMN_NAME", c.is_nullable AS "IS_NULLABLE", t.Name AS "DATA_TYPE", (c.max_length/2) AS "CHARACTER_MAXIMUM_LENGTH", c.precision AS "NUMERIC_PRECISION", c.scale AS "NUMERIC_SCALE", \'\' AS "COLUMN_TYPE" FROM sys.columns c INNER JOIN sys.types t ON c.user_type_id = t.user_type_id WHERE c.object_id = OBJECT_ID(?) AND \'\' <> ? ORDER BY c.column_id';
+ case 'sqlite':
+ return 'SELECT "name" AS "COLUMN_NAME", case when "notnull"==1 then \'no\' else \'yes\' end as "IS_NULLABLE", lower("type") AS "DATA_TYPE", 2147483647 AS "CHARACTER_MAXIMUM_LENGTH", 0 AS "NUMERIC_PRECISION", 0 AS "NUMERIC_SCALE", \'\' AS "COLUMN_TYPE" FROM pragma_table_info(?) WHERE \'\' <> ? ORDER BY "cid"';
+ }
+ }
+
+ private function getTablePrimaryKeysSQL(): string
+ {
+ switch ($this->driver) {
+ case 'mysql':
+ return 'SELECT "COLUMN_NAME" FROM "INFORMATION_SCHEMA"."KEY_COLUMN_USAGE" WHERE "CONSTRAINT_NAME" = \'PRIMARY\' AND "TABLE_NAME" = ? AND "TABLE_SCHEMA" = ?';
+ case 'pgsql':
+ return 'SELECT a.attname AS "COLUMN_NAME" FROM pg_attribute a JOIN pg_constraint c ON (c.conrelid, c.conkey[1]) = (a.attrelid, a.attnum) JOIN pg_class pgc ON pgc.oid = a.attrelid WHERE pgc.relname = ? AND \'\' <> ? AND c.contype = \'p\'';
+ case 'sqlsrv':
+ return 'SELECT c.NAME as "COLUMN_NAME" FROM sys.key_constraints kc inner join sys.objects t on t.object_id = kc.parent_object_id INNER JOIN sys.index_columns ic ON kc.parent_object_id = ic.object_id and kc.unique_index_id = ic.index_id INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id WHERE kc.type = \'PK\' and t.object_id = OBJECT_ID(?) and \'\' <> ?';
+ case 'sqlite':
+ return 'SELECT "name" as "COLUMN_NAME" FROM pragma_table_info(?) WHERE "pk"=1 AND \'\' <> ?';
+ }
+ }
+
+ private function getTableForeignKeysSQL(): string
+ {
+ switch ($this->driver) {
+ case 'mysql':
+ return 'SELECT "COLUMN_NAME", "REFERENCED_TABLE_NAME" FROM "INFORMATION_SCHEMA"."KEY_COLUMN_USAGE" WHERE "REFERENCED_TABLE_NAME" IS NOT NULL AND "TABLE_NAME" = ? AND "TABLE_SCHEMA" = ?';
+ case 'pgsql':
+ return 'SELECT a.attname AS "COLUMN_NAME", c.confrelid::regclass::text AS "REFERENCED_TABLE_NAME" FROM pg_attribute a JOIN pg_constraint c ON (c.conrelid, c.conkey[1]) = (a.attrelid, a.attnum) JOIN pg_class pgc ON pgc.oid = a.attrelid WHERE pgc.relname = ? AND \'\' <> ? AND c.contype = \'f\'';
+ case 'sqlsrv':
+ return 'SELECT COL_NAME(fc.parent_object_id, fc.parent_column_id) AS "COLUMN_NAME", OBJECT_NAME (f.referenced_object_id) AS "REFERENCED_TABLE_NAME" FROM sys.foreign_keys AS f INNER JOIN sys.foreign_key_columns AS fc ON f.OBJECT_ID = fc.constraint_object_id WHERE f.parent_object_id = OBJECT_ID(?) and \'\' <> ?';
+ case 'sqlite':
+ return 'SELECT "from" AS "COLUMN_NAME", "table" AS "REFERENCED_TABLE_NAME" FROM pragma_foreign_key_list(?) WHERE \'\' <> ?';
+ }
+ }
+
+ public function getDatabaseName(): string
+ {
+ return $this->database;
+ }
+
+ public function getTables(): array
+ {
+ $sql = $this->getTablesSQL();
+ $results = $this->query($sql, [$this->database]);
+ $tables = $this->tables;
+ $results = array_filter($results, function ($v) use ($tables) {
+ return !$tables || in_array($v['TABLE_NAME'], $tables);
+ });
+ foreach ($results as &$result) {
+ $map = [];
+ switch ($this->driver) {
+ case 'mysql':
+ $map = ['BASE TABLE' => 'table', 'VIEW' => 'view'];
+ break;
+ case 'pgsql':
+ $map = ['r' => 'table', 'v' => 'view'];
+ break;
+ case 'sqlsrv':
+ $map = ['U' => 'table', 'V' => 'view'];
+ break;
+ case 'sqlite':
+ $map = ['table' => 'table', 'view' => 'view'];
+ break;
+ }
+ $result['TABLE_TYPE'] = $map[trim($result['TABLE_TYPE'])];
+ }
+ return $results;
+ }
+
+ public function getTableColumns(string $tableName, string $type): array
+ {
+ $sql = $this->getTableColumnsSQL();
+ $results = $this->query($sql, [$tableName, $this->database]);
+ if ($type == 'view') {
+ foreach ($results as &$result) {
+ $result['IS_NULLABLE'] = false;
+ }
+ }
+ if ($this->driver == 'mysql') {
+ foreach ($results as &$result) {
+ // mysql does not properly reflect display width of types
+ preg_match('|([a-z]+)(\(([0-9]+)(,([0-9]+))?\))?|', $result['DATA_TYPE'], $matches);
+ $result['DATA_TYPE'] = $matches[1];
+ if (!$result['CHARACTER_MAXIMUM_LENGTH']) {
+ if (isset($matches[3])) {
+ $result['NUMERIC_PRECISION'] = $matches[3];
+ }
+ if (isset($matches[5])) {
+ $result['NUMERIC_SCALE'] = $matches[5];
+ }
+ }
+ }
+ }
+ if ($this->driver == 'sqlite') {
+ foreach ($results as &$result) {
+ // sqlite does not properly reflect display width of types
+ preg_match('|([a-z]+)(\(([0-9]+)(,([0-9]+))?\))?|', $result['DATA_TYPE'], $matches);
+ if (isset($matches[1])) {
+ $result['DATA_TYPE'] = $matches[1];
+ } else {
+ $result['DATA_TYPE'] = 'integer';
+ }
+ if (isset($matches[5])) {
+ $result['NUMERIC_PRECISION'] = $matches[3];
+ $result['NUMERIC_SCALE'] = $matches[5];
+ } else if (isset($matches[3])) {
+ $result['CHARACTER_MAXIMUM_LENGTH'] = $matches[3];
+ }
+ }
+ }
+ return $results;
+ }
+
+ public function getTablePrimaryKeys(string $tableName): array
+ {
+ $sql = $this->getTablePrimaryKeysSQL();
+ $results = $this->query($sql, [$tableName, $this->database]);
+ $primaryKeys = [];
+ foreach ($results as $result) {
+ $primaryKeys[] = $result['COLUMN_NAME'];
+ }
+ return $primaryKeys;
+ }
+
+ public function getTableForeignKeys(string $tableName): array
+ {
+ $sql = $this->getTableForeignKeysSQL();
+ $results = $this->query($sql, [$tableName, $this->database]);
+ $foreignKeys = [];
+ foreach ($results as $result) {
+ $foreignKeys[$result['COLUMN_NAME']] = $result['REFERENCED_TABLE_NAME'];
+ }
+ return $foreignKeys;
+ }
+
+ public function toJdbcType(string $type, string $size): string
+ {
+ return $this->typeConverter->toJdbc($type, $size);
+ }
+
+ private function query(string $sql, array $parameters): array
+ {
+ $stmt = $this->pdo->prepare($sql);
+ //echo "- $sql -- " . json_encode($parameters, JSON_UNESCAPED_UNICODE) . "\n";
+ $stmt->execute($parameters);
+ return $stmt->fetchAll();
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Database/LazyPdo.php
+namespace Tqdev\PhpCrudApi\Database {
+
+ class LazyPdo extends \PDO
+ {
+ private $dsn;
+ private $user;
+ private $password;
+ private $options;
+ private $commands;
+
+ private $pdo = null;
+
+ public function __construct(string $dsn, /*?string*/ $user = null, /*?string*/ $password = null, array $options = array())
+ {
+ $this->dsn = $dsn;
+ $this->user = $user;
+ $this->password = $password;
+ $this->options = $options;
+ $this->commands = array();
+ // explicitly NOT calling super::__construct
+ }
+
+ public function addInitCommand(string $command)/*: void*/
+ {
+ $this->commands[] = $command;
+ }
+
+ private function pdo()
+ {
+ if (!$this->pdo) {
+ $this->pdo = new \PDO($this->dsn, $this->user, $this->password, $this->options);
+ foreach ($this->commands as $command) {
+ $this->pdo->query($command);
+ }
+ }
+ return $this->pdo;
+ }
+
+ public function reconstruct(string $dsn, /*?string*/ $user = null, /*?string*/ $password = null, array $options = array()): bool
+ {
+ $this->dsn = $dsn;
+ $this->user = $user;
+ $this->password = $password;
+ $this->options = $options;
+ $this->commands = array();
+ if ($this->pdo) {
+ $this->pdo = null;
+ return true;
+ }
+ return false;
+ }
+
+ public function inTransaction(): bool
+ {
+ // Do not call parent method if there is no pdo object
+ return $this->pdo && parent::inTransaction();
+ }
+
+ public function setAttribute($attribute, $value): bool
+ {
+ if ($this->pdo) {
+ return $this->pdo()->setAttribute($attribute, $value);
+ }
+ $this->options[$attribute] = $value;
+ return true;
+ }
+
+ public function getAttribute($attribute): mixed
+ {
+ return $this->pdo()->getAttribute($attribute);
+ }
+
+ public function beginTransaction(): bool
+ {
+ return $this->pdo()->beginTransaction();
+ }
+
+ public function commit(): bool
+ {
+ return $this->pdo()->commit();
+ }
+
+ public function rollBack(): bool
+ {
+ return $this->pdo()->rollBack();
+ }
+
+ public function errorCode(): mixed
+ {
+ return $this->pdo()->errorCode();
+ }
+
+ public function errorInfo(): array
+ {
+ return $this->pdo()->errorInfo();
+ }
+
+ public function exec($query): int
+ {
+ return $this->pdo()->exec($query);
+ }
+
+ public function prepare($statement, $options = array())
+ {
+ return $this->pdo()->prepare($statement, $options);
+ }
+
+ public function quote($string, $parameter_type = null): string
+ {
+ return $this->pdo()->quote($string, $parameter_type);
+ }
+
+ public function lastInsertId(/* ?string */$name = null): string
+ {
+ return $this->pdo()->lastInsertId($name);
+ }
+
+ public function query(string $statement): \PDOStatement
+ {
+ return call_user_func_array(array($this->pdo(), 'query'), func_get_args());
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Database/TypeConverter.php
+namespace Tqdev\PhpCrudApi\Database {
+
+ class TypeConverter
+ {
+ private $driver;
+
+ public function __construct(string $driver)
+ {
+ $this->driver = $driver;
+ }
+
+ private $fromJdbc = [
+ 'mysql' => [
+ 'clob' => 'longtext',
+ 'boolean' => 'tinyint(1)',
+ 'blob' => 'longblob',
+ 'timestamp' => 'datetime',
+ ],
+ 'pgsql' => [
+ 'clob' => 'text',
+ 'blob' => 'bytea',
+ 'float' => 'real',
+ 'double' => 'double precision',
+ 'varbinary' => 'bytea',
+ ],
+ 'sqlsrv' => [
+ 'boolean' => 'bit',
+ 'varchar' => 'nvarchar',
+ 'clob' => 'ntext',
+ 'blob' => 'image',
+ 'time' => 'time(0)',
+ 'timestamp' => 'datetime2(0)',
+ 'double' => 'float',
+ 'float' => 'real',
+ ],
+ ];
+
+ private $toJdbc = [
+ 'simplified' => [
+ 'char' => 'varchar',
+ 'longvarchar' => 'clob',
+ 'nchar' => 'varchar',
+ 'nvarchar' => 'varchar',
+ 'longnvarchar' => 'clob',
+ 'binary' => 'varbinary',
+ 'longvarbinary' => 'blob',
+ 'tinyint' => 'integer',
+ 'smallint' => 'integer',
+ 'real' => 'float',
+ 'numeric' => 'decimal',
+ 'nclob' => 'clob',
+ 'time_with_timezone' => 'time',
+ 'timestamp_with_timezone' => 'timestamp',
+ ],
+ 'mysql' => [
+ 'tinyint(1)' => 'boolean',
+ 'bit(1)' => 'boolean',
+ 'tinyblob' => 'blob',
+ 'mediumblob' => 'blob',
+ 'longblob' => 'blob',
+ 'tinytext' => 'clob',
+ 'mediumtext' => 'clob',
+ 'longtext' => 'clob',
+ 'text' => 'clob',
+ 'mediumint' => 'integer',
+ 'int' => 'integer',
+ 'polygon' => 'geometry',
+ 'point' => 'geometry',
+ 'datetime' => 'timestamp',
+ 'year' => 'integer',
+ 'enum' => 'varchar',
+ 'set' => 'varchar',
+ 'json' => 'clob',
+ ],
+ 'pgsql' => [
+ 'bigserial' => 'bigint',
+ 'bit varying' => 'bit',
+ 'box' => 'geometry',
+ 'bytea' => 'blob',
+ 'bpchar' => 'char',
+ 'character varying' => 'varchar',
+ 'character' => 'char',
+ 'cidr' => 'varchar',
+ 'circle' => 'geometry',
+ 'double precision' => 'double',
+ 'inet' => 'integer',
+ //'interval [ fields ]'
+ 'json' => 'clob',
+ 'jsonb' => 'clob',
+ 'line' => 'geometry',
+ 'lseg' => 'geometry',
+ 'macaddr' => 'varchar',
+ 'money' => 'decimal',
+ 'path' => 'geometry',
+ 'point' => 'geometry',
+ 'polygon' => 'geometry',
+ 'real' => 'float',
+ 'serial' => 'integer',
+ 'text' => 'clob',
+ 'time without time zone' => 'time',
+ 'time with time zone' => 'time_with_timezone',
+ 'timestamp without time zone' => 'timestamp',
+ 'timestamp with time zone' => 'timestamp_with_timezone',
+ //'tsquery'=
+ //'tsvector'
+ //'txid_snapshot'
+ 'uuid' => 'char',
+ 'xml' => 'clob',
+ ],
+ // source: https://docs.microsoft.com/en-us/sql/connect/jdbc/using-basic-data-types?view=sql-server-2017
+ 'sqlsrv' => [
+ 'varbinary()' => 'blob',
+ 'bit' => 'boolean',
+ 'datetime' => 'timestamp',
+ 'datetime2' => 'timestamp',
+ 'float' => 'double',
+ 'image' => 'blob',
+ 'int' => 'integer',
+ 'money' => 'decimal',
+ 'ntext' => 'clob',
+ 'smalldatetime' => 'timestamp',
+ 'smallmoney' => 'decimal',
+ 'text' => 'clob',
+ 'timestamp' => 'binary',
+ 'udt' => 'varbinary',
+ 'uniqueidentifier' => 'char',
+ 'xml' => 'clob',
+ ],
+ 'sqlite' => [
+ 'tinytext' => 'clob',
+ 'text' => 'clob',
+ 'mediumtext' => 'clob',
+ 'longtext' => 'clob',
+ 'mediumint' => 'integer',
+ 'int' => 'integer',
+ 'bigint' => 'bigint',
+ 'int2' => 'smallint',
+ 'int4' => 'integer',
+ 'int8' => 'bigint',
+ 'double precision' => 'double',
+ 'datetime' => 'timestamp'
+ ],
+ ];
+
+ // source: https://docs.oracle.com/javase/9/docs/api/java/sql/Types.html
+ private $valid = [
+ //'array' => true,
+ 'bigint' => true,
+ 'binary' => true,
+ 'bit' => true,
+ 'blob' => true,
+ 'boolean' => true,
+ 'char' => true,
+ 'clob' => true,
+ //'datalink' => true,
+ 'date' => true,
+ 'decimal' => true,
+ //'distinct' => true,
+ 'double' => true,
+ 'float' => true,
+ 'integer' => true,
+ //'java_object' => true,
+ 'longnvarchar' => true,
+ 'longvarbinary' => true,
+ 'longvarchar' => true,
+ 'nchar' => true,
+ 'nclob' => true,
+ //'null' => true,
+ 'numeric' => true,
+ 'nvarchar' => true,
+ //'other' => true,
+ 'real' => true,
+ //'ref' => true,
+ //'ref_cursor' => true,
+ //'rowid' => true,
+ 'smallint' => true,
+ //'sqlxml' => true,
+ //'struct' => true,
+ 'time' => true,
+ 'time_with_timezone' => true,
+ 'timestamp' => true,
+ 'timestamp_with_timezone' => true,
+ 'tinyint' => true,
+ 'varbinary' => true,
+ 'varchar' => true,
+ // extra:
+ 'geometry' => true,
+ ];
+
+ public function toJdbc(string $type, string $size): string
+ {
+ $jdbcType = strtolower($type);
+ if (isset($this->toJdbc[$this->driver]["$jdbcType($size)"])) {
+ $jdbcType = $this->toJdbc[$this->driver]["$jdbcType($size)"];
+ }
+ if (isset($this->toJdbc[$this->driver][$jdbcType])) {
+ $jdbcType = $this->toJdbc[$this->driver][$jdbcType];
+ }
+ if (isset($this->toJdbc['simplified'][$jdbcType])) {
+ $jdbcType = $this->toJdbc['simplified'][$jdbcType];
+ }
+ if (!isset($this->valid[$jdbcType])) {
+ //throw new \Exception("Unsupported type '$jdbcType' for driver '$this->driver'");
+ $jdbcType = 'clob';
+ }
+ return $jdbcType;
+ }
+
+ public function fromJdbc(string $type): string
+ {
+ $jdbcType = strtolower($type);
+ if (isset($this->fromJdbc[$this->driver][$jdbcType])) {
+ $jdbcType = $this->fromJdbc[$this->driver][$jdbcType];
+ }
+ return $jdbcType;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/GeoJson/Feature.php
+namespace Tqdev\PhpCrudApi\GeoJson {
+
+ class Feature implements \JsonSerializable
+ {
+ private $id;
+ private $properties;
+ private $geometry;
+
+ public function __construct($id, array $properties, /*?Geometry*/ $geometry)
+ {
+ $this->id = $id;
+ $this->properties = $properties;
+ $this->geometry = $geometry;
+ }
+
+ public function serialize()
+ {
+ return [
+ 'type' => 'Feature',
+ 'id' => $this->id,
+ 'properties' => $this->properties,
+ 'geometry' => $this->geometry,
+ ];
+ }
+
+ public function jsonSerialize()
+ {
+ return $this->serialize();
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/GeoJson/FeatureCollection.php
+namespace Tqdev\PhpCrudApi\GeoJson {
+
+ class FeatureCollection implements \JsonSerializable
+ {
+ private $features;
+
+ private $results;
+
+ public function __construct(array $features, int $results)
+ {
+ $this->features = $features;
+ $this->results = $results;
+ }
+
+ public function serialize()
+ {
+ return [
+ 'type' => 'FeatureCollection',
+ 'features' => $this->features,
+ 'results' => $this->results,
+ ];
+ }
+
+ public function jsonSerialize()
+ {
+ return array_filter($this->serialize(), function ($v) {
+ return $v !== 0;
+ });
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/GeoJson/GeoJsonService.php
+namespace Tqdev\PhpCrudApi\GeoJson {
+
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\GeoJson\FeatureCollection;
+ use Tqdev\PhpCrudApi\Record\RecordService;
+
+ class GeoJsonService
+ {
+ private $reflection;
+ private $records;
+
+ public function __construct(ReflectionService $reflection, RecordService $records)
+ {
+ $this->reflection = $reflection;
+ $this->records = $records;
+ }
+
+ public function hasTable(string $table): bool
+ {
+ return $this->reflection->hasTable($table);
+ }
+
+ public function getType(string $table): string
+ {
+ return $this->reflection->getType($table);
+ }
+
+ private function getGeometryColumnName(string $tableName, array &$params): string
+ {
+ $geometryParam = isset($params['geometry']) ? $params['geometry'][0] : '';
+ $table = $this->reflection->getTable($tableName);
+ $geometryColumnName = '';
+ foreach ($table->getColumnNames() as $columnName) {
+ if ($geometryParam && $geometryParam != $columnName) {
+ continue;
+ }
+ $column = $table->getColumn($columnName);
+ if ($column->isGeometry()) {
+ $geometryColumnName = $columnName;
+ break;
+ }
+ }
+ if ($geometryColumnName) {
+ $params['mandatory'][] = $tableName . "." . $geometryColumnName;
+ }
+ return $geometryColumnName;
+ }
+
+ private function setBoudingBoxFilter(string $geometryColumnName, array &$params)
+ {
+ $boundingBox = isset($params['bbox']) ? $params['bbox'][0] : '';
+ if ($boundingBox) {
+ $c = explode(',', $boundingBox);
+ if (!isset($params['filter'])) {
+ $params['filter'] = array();
+ }
+ $params['filter'][] = "$geometryColumnName,sin,POLYGON(($c[0] $c[1],$c[2] $c[1],$c[2] $c[3],$c[0] $c[3],$c[0] $c[1]))";
+ }
+ $tile = isset($params['tile']) ? $params['tile'][0] : '';
+ if ($tile) {
+ $zxy = explode(',', $tile);
+ if (count($zxy) == 3) {
+ list($z, $x, $y) = $zxy;
+ $c = array();
+ $c = array_merge($c, $this->convertTileToLatLonOfUpperLeftCorner($z, $x, $y));
+ $c = array_merge($c, $this->convertTileToLatLonOfUpperLeftCorner($z, $x + 1, $y + 1));
+ $params['filter'][] = "$geometryColumnName,sin,POLYGON(($c[0] $c[1],$c[2] $c[1],$c[2] $c[3],$c[0] $c[3],$c[0] $c[1]))";
+ }
+ }
+ }
+
+ private function convertTileToLatLonOfUpperLeftCorner($z, $x, $y): array
+ {
+ $n = pow(2, $z);
+ $lon = $x / $n * 360.0 - 180.0;
+ $lat = rad2deg(atan(sinh(pi() * (1 - 2 * $y / $n))));
+ return [$lon, $lat];
+ }
+
+ private function convertRecordToFeature(/*object*/$record, string $primaryKeyColumnName, string $geometryColumnName)
+ {
+ $id = null;
+ if ($primaryKeyColumnName) {
+ $id = $record[$primaryKeyColumnName];
+ }
+ $geometry = null;
+ if (isset($record[$geometryColumnName])) {
+ $geometry = Geometry::fromWkt($record[$geometryColumnName]);
+ }
+ $properties = array_diff_key($record, [$primaryKeyColumnName => true, $geometryColumnName => true]);
+ return new Feature($id, $properties, $geometry);
+ }
+
+ private function getPrimaryKeyColumnName(string $tableName, array &$params): string
+ {
+ $primaryKeyColumn = $this->reflection->getTable($tableName)->getPk();
+ if (!$primaryKeyColumn) {
+ return '';
+ }
+ $primaryKeyColumnName = $primaryKeyColumn->getName();
+ $params['mandatory'][] = $tableName . "." . $primaryKeyColumnName;
+ return $primaryKeyColumnName;
+ }
+
+ public function _list(string $tableName, array $params): FeatureCollection
+ {
+ $geometryColumnName = $this->getGeometryColumnName($tableName, $params);
+ $this->setBoudingBoxFilter($geometryColumnName, $params);
+ $primaryKeyColumnName = $this->getPrimaryKeyColumnName($tableName, $params);
+ $records = $this->records->_list($tableName, $params);
+ $features = array();
+ foreach ($records->getRecords() as $record) {
+ $features[] = $this->convertRecordToFeature($record, $primaryKeyColumnName, $geometryColumnName);
+ }
+ return new FeatureCollection($features, $records->getResults());
+ }
+
+ public function read(string $tableName, string $id, array $params): Feature
+ {
+ $geometryColumnName = $this->getGeometryColumnName($tableName, $params);
+ $primaryKeyColumnName = $this->getPrimaryKeyColumnName($tableName, $params);
+ $record = $this->records->read($tableName, $id, $params);
+ return $this->convertRecordToFeature($record, $primaryKeyColumnName, $geometryColumnName);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/GeoJson/Geometry.php
+namespace Tqdev\PhpCrudApi\GeoJson {
+
+ class Geometry implements \JsonSerializable
+ {
+ private $type;
+ private $geometry;
+
+ public static $types = [
+ "Point",
+ "MultiPoint",
+ "LineString",
+ "MultiLineString",
+ "Polygon",
+ "MultiPolygon",
+ //"GeometryCollection",
+ ];
+
+ public function __construct(string $type, array $coordinates)
+ {
+ $this->type = $type;
+ $this->coordinates = $coordinates;
+ }
+
+ public static function fromWkt(string $wkt): Geometry
+ {
+ $bracket = strpos($wkt, '(');
+ $type = strtoupper(trim(substr($wkt, 0, $bracket)));
+ $supported = false;
+ foreach (Geometry::$types as $typeName) {
+ if (strtoupper($typeName) == $type) {
+ $type = $typeName;
+ $supported = true;
+ }
+ }
+ if (!$supported) {
+ throw new \Exception('Geometry type not supported: ' . $type);
+ }
+ $coordinates = substr($wkt, $bracket);
+ if (substr($type, -5) != 'Point' || ($type == 'MultiPoint' && $coordinates[1] != '(')) {
+ $coordinates = preg_replace('|([0-9\-\.]+ )+([0-9\-\.]+)|', '[\1\2]', $coordinates);
+ }
+ $coordinates = str_replace(['(', ')', ', ', ' '], ['[', ']', ',', ','], $coordinates);
+ $coordinates = json_decode($coordinates);
+ if (!$coordinates) {
+ throw new \Exception('Could not decode WKT: ' . $wkt);
+ }
+ return new Geometry($type, $coordinates);
+ }
+
+ public function serialize()
+ {
+ return [
+ 'type' => $this->type,
+ 'coordinates' => $this->coordinates,
+ ];
+ }
+
+ public function jsonSerialize()
+ {
+ return $this->serialize();
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/Base/Middleware.php
+namespace Tqdev\PhpCrudApi\Middleware\Base {
+
+ use Psr\Http\Server\MiddlewareInterface;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+
+ abstract class Middleware implements MiddlewareInterface
+ {
+ protected $next;
+ protected $responder;
+ private $properties;
+
+ public function __construct(Router $router, Responder $responder, array $properties)
+ {
+ $router->load($this);
+ $this->responder = $responder;
+ $this->properties = $properties;
+ }
+
+ protected function getArrayProperty(string $key, string $default): array
+ {
+ return array_filter(array_map('trim', explode(',', $this->getProperty($key, $default))));
+ }
+
+ protected function getMapProperty(string $key, string $default): array
+ {
+ $pairs = $this->getArrayProperty($key, $default);
+ $result = array();
+ foreach ($pairs as $pair) {
+ if (strpos($pair, ':')) {
+ list($k, $v) = explode(':', $pair, 2);
+ $result[trim($k)] = trim($v);
+ } else {
+ $result[] = trim($pair);
+ }
+ }
+ return $result;
+ }
+
+ protected function getProperty(string $key, $default)
+ {
+ return isset($this->properties[$key]) ? $this->properties[$key] : $default;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/Communication/VariableStore.php
+namespace Tqdev\PhpCrudApi\Middleware\Communication {
+
+ class VariableStore
+ {
+ public static $values = array();
+
+ public static function get(string $key)
+ {
+ if (isset(self::$values[$key])) {
+ return self::$values[$key];
+ }
+ return null;
+ }
+
+ public static function set(string $key, /* object */ $value)
+ {
+ self::$values[$key] = $value;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/Router/Router.php
+namespace Tqdev\PhpCrudApi\Middleware\Router {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+
+ interface Router extends RequestHandlerInterface
+ {
+ public function register(string $method, string $path, array $handler);
+
+ public function load(Middleware $middleware);
+
+ public function route(ServerRequestInterface $request): ResponseInterface;
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/Router/SimpleRouter.php
+namespace Tqdev\PhpCrudApi\Middleware\Router {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Tqdev\PhpCrudApi\Cache\Cache;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\Record\PathTree;
+ use Tqdev\PhpCrudApi\RequestUtils;
+ use Tqdev\PhpCrudApi\ResponseUtils;
+
+ class SimpleRouter implements Router
+ {
+ private $basePath;
+ private $responder;
+ private $cache;
+ private $ttl;
+ private $debug;
+ private $registration;
+ private $routes;
+ private $routeHandlers;
+ private $middlewares;
+
+ public function __construct(string $basePath, Responder $responder, Cache $cache, int $ttl, bool $debug)
+ {
+ $this->basePath = rtrim($this->detectBasePath($basePath), '/');
+ $this->responder = $responder;
+ $this->cache = $cache;
+ $this->ttl = $ttl;
+ $this->debug = $debug;
+ $this->registration = true;
+ $this->routes = $this->loadPathTree();
+ $this->routeHandlers = [];
+ $this->middlewares = array();
+ }
+
+ private function detectBasePath(string $basePath): string
+ {
+ if ($basePath) {
+ return $basePath;
+ }
+ if (isset($_SERVER['REQUEST_URI'])) {
+ $fullPath = urldecode(explode('?', $_SERVER['REQUEST_URI'])[0]);
+ if (isset($_SERVER['PATH_INFO'])) {
+ $path = $_SERVER['PATH_INFO'];
+ if (substr($fullPath, -1 * strlen($path)) == $path) {
+ return substr($fullPath, 0, -1 * strlen($path));
+ }
+ }
+ if ('/' . basename(__FILE__) == $fullPath) {
+ return $fullPath;
+ }
+ }
+ return '/';
+ }
+
+ private function loadPathTree(): PathTree
+ {
+ $data = $this->cache->get('PathTree');
+ if ($data != '') {
+ $tree = PathTree::fromJson(json_decode(gzuncompress($data)));
+ $this->registration = false;
+ } else {
+ $tree = new PathTree();
+ }
+ return $tree;
+ }
+
+ public function register(string $method, string $path, array $handler)
+ {
+ $routeNumber = count($this->routeHandlers);
+ $this->routeHandlers[$routeNumber] = $handler;
+ if ($this->registration) {
+ $path = trim($path, '/');
+ $parts = array();
+ if ($path) {
+ $parts = explode('/', $path);
+ }
+ array_unshift($parts, $method);
+ $this->routes->put($parts, $routeNumber);
+ }
+ }
+
+ public function load(Middleware $middleware) /*: void*/
+ {
+ array_push($this->middlewares, $middleware);
+ }
+
+ public function route(ServerRequestInterface $request): ResponseInterface
+ {
+ if ($this->registration) {
+ $data = gzcompress(json_encode($this->routes, JSON_UNESCAPED_UNICODE));
+ $this->cache->set('PathTree', $data, $this->ttl);
+ }
+
+ return $this->handle($request);
+ }
+
+ private function getRouteNumbers(ServerRequestInterface $request): array
+ {
+ $method = strtoupper($request->getMethod());
+ $path = array();
+ $segment = $method;
+ for ($i = 1; strlen($segment) > 0; $i++) {
+ array_push($path, $segment);
+ $segment = RequestUtils::getPathSegment($request, $i);
+ }
+ return $this->routes->match($path);
+ }
+
+ private function removeBasePath(ServerRequestInterface $request): ServerRequestInterface
+ {
+ $path = $request->getUri()->getPath();
+ if (substr($path, 0, strlen($this->basePath)) == $this->basePath) {
+ $path = substr($path, strlen($this->basePath));
+ $request = $request->withUri($request->getUri()->withPath($path));
+ }
+ return $request;
+ }
+
+ public function getBasePath(): string
+ {
+ return $this->basePath;
+ }
+
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $request = $this->removeBasePath($request);
+
+ if (count($this->middlewares)) {
+ $handler = array_pop($this->middlewares);
+ return $handler->process($request, $this);
+ }
+
+ $routeNumbers = $this->getRouteNumbers($request);
+ if (count($routeNumbers) == 0) {
+ return $this->responder->error(ErrorCode::ROUTE_NOT_FOUND, $request->getUri()->getPath());
+ }
+ try {
+ $response = call_user_func($this->routeHandlers[$routeNumbers[0]], $request);
+ } catch (\PDOException $e) {
+ if (strpos(strtolower($e->getMessage()), 'duplicate') !== false) {
+ $response = $this->responder->error(ErrorCode::DUPLICATE_KEY_EXCEPTION, '');
+ } elseif (strpos(strtolower($e->getMessage()), 'unique constraint') !== false) {
+ $response = $this->responder->error(ErrorCode::DUPLICATE_KEY_EXCEPTION, '');
+ } elseif (strpos(strtolower($e->getMessage()), 'default value') !== false) {
+ $response = $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
+ } elseif (strpos(strtolower($e->getMessage()), 'allow nulls') !== false) {
+ $response = $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
+ } elseif (strpos(strtolower($e->getMessage()), 'constraint') !== false) {
+ $response = $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
+ } else {
+ $response = $this->responder->error(ErrorCode::ERROR_NOT_FOUND, '');
+ }
+ if ($this->debug) {
+ $response = ResponseUtils::addExceptionHeaders($response, $e);
+ }
+ }
+ return $response;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/AjaxOnlyMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class AjaxOnlyMiddleware extends Middleware
+ {
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $method = $request->getMethod();
+ $excludeMethods = $this->getArrayProperty('excludeMethods', 'OPTIONS,GET');
+ if (!in_array($method, $excludeMethods)) {
+ $headerName = $this->getProperty('headerName', 'X-Requested-With');
+ $headerValue = $this->getProperty('headerValue', 'XMLHttpRequest');
+ if ($headerValue != RequestUtils::getHeader($request, $headerName)) {
+ return $this->responder->error(ErrorCode::ONLY_AJAX_REQUESTS_ALLOWED, $method);
+ }
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/AuthorizationMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Communication\VariableStore;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\Record\FilterInfo;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class AuthorizationMiddleware extends Middleware
+ {
+ private $reflection;
+
+ public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->reflection = $reflection;
+ }
+
+ private function handleColumns(string $operation, string $tableName) /*: void*/
+ {
+ $columnHandler = $this->getProperty('columnHandler', '');
+ if ($columnHandler) {
+ $table = $this->reflection->getTable($tableName);
+ foreach ($table->getColumnNames() as $columnName) {
+ $allowed = call_user_func($columnHandler, $operation, $tableName, $columnName);
+ if (!$allowed) {
+ $table->removeColumn($columnName);
+ }
+ }
+ }
+ }
+
+ private function handleTable(string $operation, string $tableName) /*: void*/
+ {
+ if (!$this->reflection->hasTable($tableName)) {
+ return;
+ }
+ $allowed = true;
+ $tableHandler = $this->getProperty('tableHandler', '');
+ if ($tableHandler) {
+ $allowed = call_user_func($tableHandler, $operation, $tableName);
+ }
+ if (!$allowed) {
+ $this->reflection->removeTable($tableName);
+ } else {
+ $this->handleColumns($operation, $tableName);
+ }
+ }
+
+ private function handleRecords(string $operation, string $tableName) /*: void*/
+ {
+ if (!$this->reflection->hasTable($tableName)) {
+ return;
+ }
+ $recordHandler = $this->getProperty('recordHandler', '');
+ if ($recordHandler) {
+ $query = call_user_func($recordHandler, $operation, $tableName);
+ $filters = new FilterInfo();
+ $table = $this->reflection->getTable($tableName);
+ $query = str_replace('][]=', ']=', str_replace('=', '[]=', $query));
+ parse_str($query, $params);
+ $condition = $filters->getCombinedConditions($table, $params);
+ VariableStore::set("authorization.conditions.$tableName", $condition);
+ }
+ }
+
+ private function pathHandler(string $path) /*: bool*/
+ {
+ $pathHandler = $this->getProperty('pathHandler', '');
+ return $pathHandler ? call_user_func($pathHandler, $path) : true;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $path = RequestUtils::getPathSegment($request, 1);
+
+ if (!$this->pathHandler($path)) {
+ return $this->responder->error(ErrorCode::ROUTE_NOT_FOUND, $request->getUri()->getPath());
+ }
+
+ $operation = RequestUtils::getOperation($request);
+ $tableNames = RequestUtils::getTableNames($request, $this->reflection);
+ foreach ($tableNames as $tableName) {
+ $this->handleTable($operation, $tableName);
+ if ($path == 'records') {
+ $this->handleRecords($operation, $tableName);
+ }
+ }
+ if ($path == 'openapi') {
+ VariableStore::set('authorization.tableHandler', $this->getProperty('tableHandler', ''));
+ VariableStore::set('authorization.columnHandler', $this->getProperty('columnHandler', ''));
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/BasicAuthMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class BasicAuthMiddleware extends Middleware
+ {
+ private function hasCorrectPassword(string $username, string $password, array &$passwords): bool
+ {
+ $hash = isset($passwords[$username]) ? $passwords[$username] : false;
+ if ($hash && password_verify($password, $hash)) {
+ if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
+ $passwords[$username] = password_hash($password, PASSWORD_DEFAULT);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private function getValidUsername(string $username, string $password, string $passwordFile): string
+ {
+ $passwords = $this->readPasswords($passwordFile);
+ $valid = $this->hasCorrectPassword($username, $password, $passwords);
+ $this->writePasswords($passwordFile, $passwords);
+ return $valid ? $username : '';
+ }
+
+ private function readPasswords(string $passwordFile): array
+ {
+ $passwords = [];
+ $passwordLines = file($passwordFile);
+ foreach ($passwordLines as $passwordLine) {
+ if (strpos($passwordLine, ':') !== false) {
+ list($username, $hash) = explode(':', trim($passwordLine), 2);
+ if (strlen($hash) > 0 && $hash[0] != '$') {
+ $hash = password_hash($hash, PASSWORD_DEFAULT);
+ }
+ $passwords[$username] = $hash;
+ }
+ }
+ return $passwords;
+ }
+
+ private function writePasswords(string $passwordFile, array $passwords): bool
+ {
+ $success = false;
+ $passwordFileContents = '';
+ foreach ($passwords as $username => $hash) {
+ $passwordFileContents .= "$username:$hash\n";
+ }
+ if (file_get_contents($passwordFile) != $passwordFileContents) {
+ $success = file_put_contents($passwordFile, $passwordFileContents) !== false;
+ }
+ return $success;
+ }
+
+ private function getAuthorizationCredentials(ServerRequestInterface $request): string
+ {
+ if (isset($_SERVER['PHP_AUTH_USER'])) {
+ return $_SERVER['PHP_AUTH_USER'] . ':' . $_SERVER['PHP_AUTH_PW'];
+ }
+ $header = RequestUtils::getHeader($request, 'Authorization');
+ $parts = explode(' ', trim($header), 2);
+ if (count($parts) != 2) {
+ return '';
+ }
+ if ($parts[0] != 'Basic') {
+ return '';
+ }
+ return base64_decode(strtr($parts[1], '-_', '+/'));
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ if (session_status() == PHP_SESSION_NONE) {
+ if (!headers_sent()) {
+ $sessionName = $this->getProperty('sessionName', '');
+ if ($sessionName) {
+ session_name($sessionName);
+ }
+ session_start();
+ }
+ }
+ $credentials = $this->getAuthorizationCredentials($request);
+ if ($credentials) {
+ list($username, $password) = array('', '');
+ if (strpos($credentials, ':') !== false) {
+ list($username, $password) = explode(':', $credentials, 2);
+ }
+ $passwordFile = $this->getProperty('passwordFile', '.htpasswd');
+ $validUser = $this->getValidUsername($username, $password, $passwordFile);
+ $_SESSION['username'] = $validUser;
+ if (!$validUser) {
+ return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username);
+ }
+ if (!headers_sent()) {
+ session_regenerate_id();
+ }
+ }
+ if (!isset($_SESSION['username']) || !$_SESSION['username']) {
+ $authenticationMode = $this->getProperty('mode', 'required');
+ if ($authenticationMode == 'required') {
+ $response = $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, '');
+ $realm = $this->getProperty('realm', 'Username and password required');
+ $response = $response->withHeader('WWW-Authenticate', "Basic realm=\"$realm\"");
+ return $response;
+ }
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/CorsMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\ResponseFactory;
+ use Tqdev\PhpCrudApi\ResponseUtils;
+
+ class CorsMiddleware extends Middleware
+ {
+ private $debug;
+
+ public function __construct(Router $router, Responder $responder, array $properties, bool $debug)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->debug = $debug;
+ }
+
+ private function isOriginAllowed(string $origin, string $allowedOrigins): bool
+ {
+ $found = false;
+ foreach (explode(',', $allowedOrigins) as $allowedOrigin) {
+ $hostname = preg_quote(strtolower(trim($allowedOrigin)));
+ $regex = '/^' . str_replace('\*', '.*', $hostname) . '$/';
+ if (preg_match($regex, $origin)) {
+ $found = true;
+ break;
+ }
+ }
+ return $found;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $method = $request->getMethod();
+ $origin = count($request->getHeader('Origin')) ? $request->getHeader('Origin')[0] : '';
+ $allowedOrigins = $this->getProperty('allowedOrigins', '*');
+ if ($origin && !$this->isOriginAllowed($origin, $allowedOrigins)) {
+ $response = $this->responder->error(ErrorCode::ORIGIN_FORBIDDEN, $origin);
+ } elseif ($method == 'OPTIONS') {
+ $response = ResponseFactory::fromStatus(ResponseFactory::OK);
+ $allowHeaders = $this->getProperty('allowHeaders', 'Content-Type, X-XSRF-TOKEN, X-Authorization');
+ if ($this->debug) {
+ $allowHeaders = implode(', ', array_filter([$allowHeaders, 'X-Exception-Name, X-Exception-Message, X-Exception-File']));
+ }
+ if ($allowHeaders) {
+ $response = $response->withHeader('Access-Control-Allow-Headers', $allowHeaders);
+ }
+ $allowMethods = $this->getProperty('allowMethods', 'OPTIONS, GET, PUT, POST, DELETE, PATCH');
+ if ($allowMethods) {
+ $response = $response->withHeader('Access-Control-Allow-Methods', $allowMethods);
+ }
+ $allowCredentials = $this->getProperty('allowCredentials', 'true');
+ if ($allowCredentials) {
+ $response = $response->withHeader('Access-Control-Allow-Credentials', $allowCredentials);
+ }
+ $maxAge = $this->getProperty('maxAge', '1728000');
+ if ($maxAge) {
+ $response = $response->withHeader('Access-Control-Max-Age', $maxAge);
+ }
+ $exposeHeaders = $this->getProperty('exposeHeaders', '');
+ if ($this->debug) {
+ $exposeHeaders = implode(', ', array_filter([$exposeHeaders, 'X-Exception-Name, X-Exception-Message, X-Exception-File']));
+ }
+ if ($exposeHeaders) {
+ $response = $response->withHeader('Access-Control-Expose-Headers', $exposeHeaders);
+ }
+ } else {
+ $response = null;
+ try {
+ $response = $next->handle($request);
+ } catch (\Throwable $e) {
+ $response = $this->responder->error(ErrorCode::ERROR_NOT_FOUND, $e->getMessage());
+ if ($this->debug) {
+ $response = ResponseUtils::addExceptionHeaders($response, $e);
+ }
+ }
+ }
+ if ($origin) {
+ $allowCredentials = $this->getProperty('allowCredentials', 'true');
+ if ($allowCredentials) {
+ $response = $response->withHeader('Access-Control-Allow-Credentials', $allowCredentials);
+ }
+ $response = $response->withHeader('Access-Control-Allow-Origin', $origin);
+ }
+ return $response;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/CustomizationMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class CustomizationMiddleware extends Middleware
+ {
+ private $reflection;
+
+ public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->reflection = $reflection;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $operation = RequestUtils::getOperation($request);
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ $beforeHandler = $this->getProperty('beforeHandler', '');
+ $environment = (object) array();
+ if ($beforeHandler !== '') {
+ $result = call_user_func($beforeHandler, $operation, $tableName, $request, $environment);
+ $request = $result ?: $request;
+ }
+ $response = $next->handle($request);
+ $afterHandler = $this->getProperty('afterHandler', '');
+ if ($afterHandler !== '') {
+ $result = call_user_func($afterHandler, $operation, $tableName, $response, $environment);
+ $response = $result ?: $response;
+ }
+ return $response;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/DbAuthMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Database\GenericDB;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\Record\Condition\ColumnCondition;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\Record\OrderingInfo;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class DbAuthMiddleware extends Middleware
+ {
+ private $reflection;
+ private $db;
+ private $ordering;
+
+ public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection, GenericDB $db)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->reflection = $reflection;
+ $this->db = $db;
+ $this->ordering = new OrderingInfo();
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ if (session_status() == PHP_SESSION_NONE) {
+ if (!headers_sent()) {
+ $sessionName = $this->getProperty('sessionName', '');
+ if ($sessionName) {
+ session_name($sessionName);
+ }
+ session_start();
+ }
+ }
+ $path = RequestUtils::getPathSegment($request, 1);
+ $method = $request->getMethod();
+ if ($method == 'POST' && in_array($path, ['login', 'register', 'password'])) {
+ $body = $request->getParsedBody();
+ $username = isset($body->username) ? $body->username : '';
+ $password = isset($body->password) ? $body->password : '';
+ $newPassword = isset($body->newPassword) ? $body->newPassword : '';
+ $tableName = $this->getProperty('usersTable', 'users');
+ $table = $this->reflection->getTable($tableName);
+ $usernameColumnName = $this->getProperty('usernameColumn', 'username');
+ $usernameColumn = $table->getColumn($usernameColumnName);
+ $passwordColumnName = $this->getProperty('passwordColumn', 'password');
+ $passwordLength = $this->getProperty('passwordLength', '12');
+ $pkName = $table->getPk()->getName();
+ $registerUser = $this->getProperty('registerUser', '');
+ $condition = new ColumnCondition($usernameColumn, 'eq', $username);
+ $returnedColumns = $this->getProperty('returnedColumns', '');
+ if (!$returnedColumns) {
+ $columnNames = $table->getColumnNames();
+ } else {
+ $columnNames = array_map('trim', explode(',', $returnedColumns));
+ $columnNames[] = $passwordColumnName;
+ $columnNames[] = $pkName;
+ }
+ $columnOrdering = $this->ordering->getDefaultColumnOrdering($table);
+ if ($path == 'register') {
+ if (!$registerUser) {
+ return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username);
+ }
+ if (strlen($password) < $passwordLength) {
+ return $this->responder->error(ErrorCode::PASSWORD_TOO_SHORT, $passwordLength);
+ }
+ $users = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, 0, 1);
+ if (!empty($users)) {
+ return $this->responder->error(ErrorCode::USER_ALREADY_EXIST, $username);
+ }
+ $data = json_decode($registerUser, true);
+ $data = is_array($data) ? $data : [];
+ $data[$usernameColumnName] = $username;
+ $data[$passwordColumnName] = password_hash($password, PASSWORD_DEFAULT);
+ $this->db->createSingle($table, $data);
+ $users = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, 0, 1);
+ foreach ($users as $user) {
+ unset($user[$passwordColumnName]);
+ return $this->responder->success($user);
+ }
+ return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username);
+ }
+ if ($path == 'login') {
+ $users = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, 0, 1);
+ foreach ($users as $user) {
+ if (password_verify($password, $user[$passwordColumnName]) == 1) {
+ if (!headers_sent()) {
+ session_regenerate_id(true);
+ }
+ unset($user[$passwordColumnName]);
+ $_SESSION['user'] = $user;
+ return $this->responder->success($user);
+ }
+ }
+ return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username);
+ }
+ if ($path == 'password') {
+ if ($username != ($_SESSION['user'][$usernameColumnName] ?? '')) {
+ return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username);
+ }
+ if (strlen($newPassword) < $passwordLength) {
+ return $this->responder->error(ErrorCode::PASSWORD_TOO_SHORT, $passwordLength);
+ }
+ $users = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, 0, 1);
+ foreach ($users as $user) {
+ if (password_verify($password, $user[$passwordColumnName]) == 1) {
+ if (!headers_sent()) {
+ session_regenerate_id(true);
+ }
+ $data = [$passwordColumnName => password_hash($newPassword, PASSWORD_DEFAULT)];
+ $this->db->updateSingle($table, $data, $user[$pkName]);
+ unset($user[$passwordColumnName]);
+ return $this->responder->success($user);
+ }
+ }
+ return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username);
+ }
+ }
+ if ($method == 'POST' && $path == 'logout') {
+ if (isset($_SESSION['user'])) {
+ $user = $_SESSION['user'];
+ unset($_SESSION['user']);
+ if (session_status() != PHP_SESSION_NONE) {
+ session_destroy();
+ }
+ return $this->responder->success($user);
+ }
+ return $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, '');
+ }
+ if ($method == 'GET' && $path == 'me') {
+ if (isset($_SESSION['user'])) {
+ return $this->responder->success($_SESSION['user']);
+ }
+ return $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, '');
+ }
+ if (!isset($_SESSION['user']) || !$_SESSION['user']) {
+ $authenticationMode = $this->getProperty('mode', 'required');
+ if ($authenticationMode == 'required') {
+ return $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, '');
+ }
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/FirewallMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+
+ class FirewallMiddleware extends Middleware
+ {
+ private function ipMatch(string $ip, string $cidr): bool
+ {
+ if (strpos($cidr, '/') !== false) {
+ list($subnet, $mask) = explode('/', trim($cidr));
+ if ((ip2long($ip) & ~((1 << (32 - $mask)) - 1)) == ip2long($subnet)) {
+ return true;
+ }
+ } else {
+ if (ip2long($ip) == ip2long($cidr)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private function isIpAllowed(string $ipAddress, string $allowedIpAddresses): bool
+ {
+ foreach (explode(',', $allowedIpAddresses) as $allowedIp) {
+ if ($this->ipMatch($ipAddress, $allowedIp)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $reverseProxy = $this->getProperty('reverseProxy', '');
+ if ($reverseProxy) {
+ $ipAddress = array_pop(explode(',', $request->getHeader('X-Forwarded-For')));
+ } elseif (isset($_SERVER['REMOTE_ADDR'])) {
+ $ipAddress = $_SERVER['REMOTE_ADDR'];
+ } else {
+ $ipAddress = '127.0.0.1';
+ }
+ $allowedIpAddresses = $this->getProperty('allowedIpAddresses', '');
+ if (!$this->isIpAllowed($ipAddress, $allowedIpAddresses)) {
+ $response = $this->responder->error(ErrorCode::TEMPORARY_OR_PERMANENTLY_BLOCKED, '');
+ } else {
+ $response = $next->handle($request);
+ }
+ return $response;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/IpAddressMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class IpAddressMiddleware extends Middleware
+ {
+ private $reflection;
+
+ public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->reflection = $reflection;
+ }
+
+ private function callHandler($record, string $operation, ReflectedTable $table) /*: object */
+ {
+ $context = (array) $record;
+ $columnNames = $this->getProperty('columns', '');
+ if ($columnNames) {
+ foreach (explode(',', $columnNames) as $columnName) {
+ if ($table->hasColumn($columnName)) {
+ if ($operation == 'create') {
+ $context[$columnName] = $_SERVER['REMOTE_ADDR'];
+ } else {
+ unset($context[$columnName]);
+ }
+ }
+ }
+ }
+ return (object) $context;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $operation = RequestUtils::getOperation($request);
+ if (in_array($operation, ['create', 'update', 'increment'])) {
+ $tableNames = $this->getProperty('tables', '');
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ if (!$tableNames || in_array($tableName, explode(',', $tableNames))) {
+ if ($this->reflection->hasTable($tableName)) {
+ $record = $request->getParsedBody();
+ if ($record !== null) {
+ $table = $this->reflection->getTable($tableName);
+ if (is_array($record)) {
+ foreach ($record as &$r) {
+ $r = $this->callHandler($r, $operation, $table);
+ }
+ } else {
+ $record = $this->callHandler($record, $operation, $table);
+ }
+ $request = $request->withParsedBody($record);
+ }
+ }
+ }
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/JoinLimitsMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Communication\VariableStore;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class JoinLimitsMiddleware extends Middleware
+ {
+ private $reflection;
+
+ public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->reflection = $reflection;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $operation = RequestUtils::getOperation($request);
+ $params = RequestUtils::getParams($request);
+ if (in_array($operation, ['read', 'list']) && isset($params['join'])) {
+ $maxDepth = (int) $this->getProperty('depth', '3');
+ $maxTables = (int) $this->getProperty('tables', '10');
+ $maxRecords = (int) $this->getProperty('records', '1000');
+ $tableCount = 0;
+ $joinPaths = array();
+ for ($i = 0; $i < count($params['join']); $i++) {
+ $joinPath = array();
+ $tables = explode(',', $params['join'][$i]);
+ for ($depth = 0; $depth < min($maxDepth, count($tables)); $depth++) {
+ array_push($joinPath, $tables[$depth]);
+ $tableCount += 1;
+ if ($tableCount == $maxTables) {
+ break;
+ }
+ }
+ array_push($joinPaths, implode(',', $joinPath));
+ if ($tableCount == $maxTables) {
+ break;
+ }
+ }
+ $params['join'] = $joinPaths;
+ $request = RequestUtils::setParams($request, $params);
+ VariableStore::set("joinLimits.maxRecords", $maxRecords);
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/JwtAuthMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class JwtAuthMiddleware extends Middleware
+ {
+ private function getVerifiedClaims(string $token, int $time, int $leeway, int $ttl, array $secrets, array $requirements): array
+ {
+ $algorithms = array(
+ 'HS256' => 'sha256',
+ 'HS384' => 'sha384',
+ 'HS512' => 'sha512',
+ 'RS256' => 'sha256',
+ 'RS384' => 'sha384',
+ 'RS512' => 'sha512',
+ );
+ $token = explode('.', $token);
+ if (count($token) < 3) {
+ return array();
+ }
+ $header = json_decode(base64_decode(strtr($token[0], '-_', '+/')), true);
+ $kid = 0;
+ if (isset($header['kid'])) {
+ $kid = $header['kid'];
+ }
+ if (!isset($secrets[$kid])) {
+ return array();
+ }
+ $secret = $secrets[$kid];
+ if ($header['typ'] != 'JWT') {
+ return array();
+ }
+ $algorithm = $header['alg'];
+ if (!isset($algorithms[$algorithm])) {
+ return array();
+ }
+ if (!empty($requirements['alg']) && !in_array($algorithm, $requirements['alg'])) {
+ return array();
+ }
+ $hmac = $algorithms[$algorithm];
+ $signature = base64_decode(strtr($token[2], '-_', '+/'));
+ $data = "$token[0].$token[1]";
+ switch ($algorithm[0]) {
+ case 'H':
+ $hash = hash_hmac($hmac, $data, $secret, true);
+ $equals = hash_equals($hash, $signature);
+ if (!$equals) {
+ return array();
+ }
+ break;
+ case 'R':
+ $equals = openssl_verify($data, $signature, $secret, $hmac) == 1;
+ if (!$equals) {
+ return array();
+ }
+ break;
+ }
+ $claims = json_decode(base64_decode(strtr($token[1], '-_', '+/')), true);
+ if (!$claims) {
+ return array();
+ }
+ foreach ($requirements as $field => $values) {
+ if (!empty($values)) {
+ if ($field != 'alg') {
+ if (!isset($claims[$field]) || !in_array($claims[$field], $values)) {
+ return array();
+ }
+ }
+ }
+ }
+ if (isset($claims['nbf']) && $time + $leeway < $claims['nbf']) {
+ return array();
+ }
+ if (isset($claims['iat']) && $time + $leeway < $claims['iat']) {
+ return array();
+ }
+ if (isset($claims['exp']) && $time - $leeway > $claims['exp']) {
+ return array();
+ }
+ if (isset($claims['iat']) && !isset($claims['exp'])) {
+ if ($time - $leeway > $claims['iat'] + $ttl) {
+ return array();
+ }
+ }
+ return $claims;
+ }
+
+ private function getClaims(string $token): array
+ {
+ $time = (int) $this->getProperty('time', time());
+ $leeway = (int) $this->getProperty('leeway', '5');
+ $ttl = (int) $this->getProperty('ttl', '30');
+ $secrets = $this->getMapProperty('secrets', '');
+ if (!$secrets) {
+ $secrets = [$this->getProperty('secret', '')];
+ }
+ $requirements = array(
+ 'alg' => $this->getArrayProperty('algorithms', ''),
+ 'aud' => $this->getArrayProperty('audiences', ''),
+ 'iss' => $this->getArrayProperty('issuers', ''),
+ );
+ return $this->getVerifiedClaims($token, $time, $leeway, $ttl, $secrets, $requirements);
+ }
+
+ private function getAuthorizationToken(ServerRequestInterface $request): string
+ {
+ $headerName = $this->getProperty('header', 'X-Authorization');
+ $headerValue = RequestUtils::getHeader($request, $headerName);
+ $parts = explode(' ', trim($headerValue), 2);
+ if (count($parts) != 2) {
+ return '';
+ }
+ if ($parts[0] != 'Bearer') {
+ return '';
+ }
+ return $parts[1];
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ if (session_status() == PHP_SESSION_NONE) {
+ if (!headers_sent()) {
+ $sessionName = $this->getProperty('sessionName', '');
+ if ($sessionName) {
+ session_name($sessionName);
+ }
+ session_start();
+ }
+ }
+ $token = $this->getAuthorizationToken($request);
+ if ($token) {
+ $claims = $this->getClaims($token);
+ $_SESSION['claims'] = $claims;
+ if (empty($claims)) {
+ return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, 'JWT');
+ }
+ if (!headers_sent()) {
+ session_regenerate_id();
+ }
+ }
+ if (empty($_SESSION['claims'])) {
+ $authenticationMode = $this->getProperty('mode', 'required');
+ if ($authenticationMode == 'required') {
+ return $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, '');
+ }
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/MultiTenancyMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Communication\VariableStore;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\Record\Condition\ColumnCondition;
+ use Tqdev\PhpCrudApi\Record\Condition\Condition;
+ use Tqdev\PhpCrudApi\Record\Condition\NoCondition;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class MultiTenancyMiddleware extends Middleware
+ {
+ private $reflection;
+
+ public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->reflection = $reflection;
+ }
+
+ private function getCondition(string $tableName, array $pairs): Condition
+ {
+ $condition = new NoCondition();
+ $table = $this->reflection->getTable($tableName);
+ foreach ($pairs as $k => $v) {
+ $condition = $condition->_and(new ColumnCondition($table->getColumn($k), 'eq', $v));
+ }
+ return $condition;
+ }
+
+ private function getPairs($handler, string $operation, string $tableName): array
+ {
+ $result = array();
+ $pairs = call_user_func($handler, $operation, $tableName) ?: [];
+ $table = $this->reflection->getTable($tableName);
+ foreach ($pairs as $k => $v) {
+ if ($table->hasColumn($k)) {
+ $result[$k] = $v;
+ }
+ }
+ return $result;
+ }
+
+ private function handleRecord(ServerRequestInterface $request, string $operation, array $pairs): ServerRequestInterface
+ {
+ $record = $request->getParsedBody();
+ if ($record === null) {
+ return $request;
+ }
+ $multi = is_array($record);
+ $records = $multi ? $record : [$record];
+ foreach ($records as &$record) {
+ foreach ($pairs as $column => $value) {
+ if ($operation == 'create') {
+ $record->$column = $value;
+ } else {
+ if (isset($record->$column)) {
+ unset($record->$column);
+ }
+ }
+ }
+ }
+ return $request->withParsedBody($multi ? $records : $records[0]);
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $handler = $this->getProperty('handler', '');
+ if ($handler !== '') {
+ $path = RequestUtils::getPathSegment($request, 1);
+ if ($path == 'records') {
+ $operation = RequestUtils::getOperation($request);
+ $tableNames = RequestUtils::getTableNames($request, $this->reflection);
+ foreach ($tableNames as $i => $tableName) {
+ if (!$this->reflection->hasTable($tableName)) {
+ continue;
+ }
+ $pairs = $this->getPairs($handler, $operation, $tableName);
+ if ($i == 0) {
+ if (in_array($operation, ['create', 'update', 'increment'])) {
+ $request = $this->handleRecord($request, $operation, $pairs);
+ }
+ }
+ $condition = $this->getCondition($tableName, $pairs);
+ VariableStore::set("multiTenancy.conditions.$tableName", $condition);
+ }
+ }
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/PageLimitsMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class PageLimitsMiddleware extends Middleware
+ {
+ private $reflection;
+
+ public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->reflection = $reflection;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $operation = RequestUtils::getOperation($request);
+ if ($operation == 'list') {
+ $params = RequestUtils::getParams($request);
+ $maxPage = (int) $this->getProperty('pages', '100');
+ if (isset($params['page']) && $params['page'] && $maxPage > 0) {
+ if (strpos($params['page'][0], ',') === false) {
+ $page = $params['page'][0];
+ } else {
+ list($page, $size) = explode(',', $params['page'][0], 2);
+ }
+ if ($page > $maxPage) {
+ return $this->responder->error(ErrorCode::PAGINATION_FORBIDDEN, '');
+ }
+ }
+ $maxSize = (int) $this->getProperty('records', '1000');
+ if (!isset($params['size']) || !$params['size'] && $maxSize > 0) {
+ $params['size'] = array($maxSize);
+ } else {
+ $params['size'] = array(min($params['size'][0], $maxSize));
+ }
+ $request = RequestUtils::setParams($request, $params);
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/ReconnectMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Database\GenericDB;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+
+ class ReconnectMiddleware extends Middleware
+ {
+ private $reflection;
+ private $db;
+
+ public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection, GenericDB $db)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->reflection = $reflection;
+ $this->db = $db;
+ }
+
+ private function getDriver(): string
+ {
+ $driverHandler = $this->getProperty('driverHandler', '');
+ if ($driverHandler) {
+ return call_user_func($driverHandler);
+ }
+ return '';
+ }
+
+ private function getAddress(): string
+ {
+ $addressHandler = $this->getProperty('addressHandler', '');
+ if ($addressHandler) {
+ return call_user_func($addressHandler);
+ }
+ return '';
+ }
+
+ private function getPort(): int
+ {
+ $portHandler = $this->getProperty('portHandler', '');
+ if ($portHandler) {
+ return call_user_func($portHandler);
+ }
+ return 0;
+ }
+
+ private function getDatabase(): string
+ {
+ $databaseHandler = $this->getProperty('databaseHandler', '');
+ if ($databaseHandler) {
+ return call_user_func($databaseHandler);
+ }
+ return '';
+ }
+
+ private function getTables(): array
+ {
+ $tablesHandler = $this->getProperty('tablesHandler', '');
+ if ($tablesHandler) {
+ return call_user_func($tablesHandler);
+ }
+ return [];
+ }
+
+ private function getUsername(): string
+ {
+ $usernameHandler = $this->getProperty('usernameHandler', '');
+ if ($usernameHandler) {
+ return call_user_func($usernameHandler);
+ }
+ return '';
+ }
+
+ private function getPassword(): string
+ {
+ $passwordHandler = $this->getProperty('passwordHandler', '');
+ if ($passwordHandler) {
+ return call_user_func($passwordHandler);
+ }
+ return '';
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $driver = $this->getDriver();
+ $address = $this->getAddress();
+ $port = $this->getPort();
+ $database = $this->getDatabase();
+ $tables = $this->getTables();
+ $username = $this->getUsername();
+ $password = $this->getPassword();
+ if ($driver || $address || $port || $database || $tables || $username || $password) {
+ $this->db->reconstruct($driver, $address, $port, $database, $tables, $username, $password);
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/SanitationMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class SanitationMiddleware extends Middleware
+ {
+ private $reflection;
+
+ public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->reflection = $reflection;
+ }
+
+ private function callHandler($handler, $record, string $operation, ReflectedTable $table) /*: object */
+ {
+ $context = (array) $record;
+ $tableName = $table->getName();
+ foreach ($context as $columnName => &$value) {
+ if ($table->hasColumn($columnName)) {
+ $column = $table->getColumn($columnName);
+ $value = call_user_func($handler, $operation, $tableName, $column->serialize(), $value);
+ $value = $this->sanitizeType($table, $column, $value);
+ }
+ }
+ return (object) $context;
+ }
+
+ private function sanitizeType(ReflectedTable $table, ReflectedColumn $column, $value)
+ {
+ $tables = $this->getArrayProperty('tables', 'all');
+ $types = $this->getArrayProperty('types', 'all');
+ if (
+ (in_array('all', $tables) || in_array($table->getName(), $tables)) &&
+ (in_array('all', $types) || in_array($column->getType(), $types))
+ ) {
+ if (is_null($value)) {
+ return $value;
+ }
+ if (is_string($value)) {
+ $newValue = null;
+ switch ($column->getType()) {
+ case 'integer':
+ case 'bigint':
+ $newValue = filter_var(trim($value), FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
+ break;
+ case 'decimal':
+ $newValue = filter_var(trim($value), FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE);
+ if (is_float($newValue)) {
+ $newValue = number_format($newValue, $column->getScale(), '.', '');
+ }
+ break;
+ case 'float':
+ case 'double':
+ $newValue = filter_var(trim($value), FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE);
+ break;
+ case 'boolean':
+ $newValue = filter_var(trim($value), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
+ break;
+ case 'date':
+ $time = strtotime(trim($value));
+ if ($time !== false) {
+ $newValue = date('Y-m-d', $time);
+ }
+ break;
+ case 'time':
+ $time = strtotime(trim($value));
+ if ($time !== false) {
+ $newValue = date('H:i:s', $time);
+ }
+ break;
+ case 'timestamp':
+ $time = strtotime(trim($value));
+ if ($time !== false) {
+ $newValue = date('Y-m-d H:i:s', $time);
+ }
+ break;
+ case 'blob':
+ case 'varbinary':
+ // allow base64url format
+ $newValue = strtr(trim($value), '-_', '+/');
+ break;
+ case 'clob':
+ case 'varchar':
+ $newValue = $value;
+ break;
+ case 'geometry':
+ $newValue = trim($value);
+ break;
+ }
+ if (!is_null($newValue)) {
+ $value = $newValue;
+ }
+ } else {
+ switch ($column->getType()) {
+ case 'integer':
+ case 'bigint':
+ if (is_float($value)) {
+ $value = (int) round($value);
+ }
+ break;
+ case 'decimal':
+ if (is_float($value) || is_int($value)) {
+ $value = number_format((float) $value, $column->getScale(), '.', '');
+ }
+ break;
+ }
+ }
+ // post process
+ }
+ return $value;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $operation = RequestUtils::getOperation($request);
+ if (in_array($operation, ['create', 'update', 'increment'])) {
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ if ($this->reflection->hasTable($tableName)) {
+ $record = $request->getParsedBody();
+ if ($record !== null) {
+ $handler = $this->getProperty('handler', '');
+ if ($handler !== '') {
+ $table = $this->reflection->getTable($tableName);
+ if (is_array($record)) {
+ foreach ($record as &$r) {
+ $r = $this->callHandler($handler, $r, $operation, $table);
+ }
+ } else {
+ $record = $this->callHandler($handler, $record, $operation, $table);
+ }
+ $request = $request->withParsedBody($record);
+ }
+ }
+ }
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/SslRedirectMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\ResponseFactory;
+
+ class SslRedirectMiddleware extends Middleware
+ {
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $uri = $request->getUri();
+ $scheme = $uri->getScheme();
+ if ($scheme == 'http') {
+ $uri = $request->getUri();
+ $uri = $uri->withScheme('https');
+ $response = ResponseFactory::fromStatus(301);
+ $response = $response->withHeader('Location', $uri->__toString());
+ } else {
+ $response = $next->handle($request);
+ }
+ return $response;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/ValidationMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\RequestUtils;
+
+ class ValidationMiddleware extends Middleware
+ {
+ private $reflection;
+
+ public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->reflection = $reflection;
+ }
+
+ private function callHandler($handler, $record, string $operation, ReflectedTable $table) /*: ResponseInterface?*/
+ {
+ $context = (array) $record;
+ $details = array();
+ $tableName = $table->getName();
+ foreach ($context as $columnName => $value) {
+ if ($table->hasColumn($columnName)) {
+ $column = $table->getColumn($columnName);
+ $valid = call_user_func($handler, $operation, $tableName, $column->serialize(), $value, $context);
+ if ($valid === true || $valid === '') {
+ $valid = $this->validateType($table, $column, $value);
+ }
+ if ($valid !== true && $valid !== '') {
+ $details[$columnName] = $valid;
+ }
+ }
+ }
+ if (count($details) > 0) {
+ return $this->responder->error(ErrorCode::INPUT_VALIDATION_FAILED, $tableName, $details);
+ }
+ return null;
+ }
+
+ private function validateType(ReflectedTable $table, ReflectedColumn $column, $value)
+ {
+ $tables = $this->getArrayProperty('tables', 'all');
+ $types = $this->getArrayProperty('types', 'all');
+ if (
+ (in_array('all', $tables) || in_array($table->getName(), $tables)) &&
+ (in_array('all', $types) || in_array($column->getType(), $types))
+ ) {
+ if (is_null($value)) {
+ return ($column->getNullable() ? true : "cannot be null");
+ }
+ if (is_string($value)) {
+ // check for whitespace
+ switch ($column->getType()) {
+ case 'varchar':
+ case 'clob':
+ break;
+ default:
+ if (strlen(trim($value)) != strlen($value)) {
+ return 'illegal whitespace';
+ }
+ break;
+ }
+ // try to parse
+ switch ($column->getType()) {
+ case 'integer':
+ case 'bigint':
+ if (
+ filter_var($value, FILTER_SANITIZE_NUMBER_INT) !== $value ||
+ filter_var($value, FILTER_VALIDATE_INT) === false
+ ) {
+ return 'invalid integer';
+ }
+ break;
+ case 'decimal':
+ if (strpos($value, '.') !== false) {
+ list($whole, $decimals) = explode('.', ltrim($value, '-'), 2);
+ } else {
+ list($whole, $decimals) = array(ltrim($value, '-'), '');
+ }
+ if (strlen($whole) > 0 && !ctype_digit($whole)) {
+ return 'invalid decimal';
+ }
+ if (strlen($decimals) > 0 && !ctype_digit($decimals)) {
+ return 'invalid decimal';
+ }
+ if (strlen($whole) > $column->getPrecision() - $column->getScale()) {
+ return 'decimal too large';
+ }
+ if (strlen($decimals) > $column->getScale()) {
+ return 'decimal too precise';
+ }
+ break;
+ case 'float':
+ case 'double':
+ if (
+ filter_var($value, FILTER_SANITIZE_NUMBER_FLOAT) !== $value ||
+ filter_var($value, FILTER_VALIDATE_FLOAT) === false
+ ) {
+ return 'invalid float';
+ }
+ break;
+ case 'boolean':
+ if (!in_array(strtolower($value), array('true', 'false'))) {
+ return 'invalid boolean';
+ }
+ break;
+ case 'date':
+ if (date_create_from_format('Y-m-d', $value) === false) {
+ return 'invalid date';
+ }
+ break;
+ case 'time':
+ if (date_create_from_format('H:i:s', $value) === false) {
+ return 'invalid time';
+ }
+ break;
+ case 'timestamp':
+ if (date_create_from_format('Y-m-d H:i:s', $value) === false) {
+ return 'invalid timestamp';
+ }
+ break;
+ case 'clob':
+ case 'varchar':
+ if ($column->hasLength() && mb_strlen($value, 'UTF-8') > $column->getLength()) {
+ return 'string too long';
+ }
+ break;
+ case 'blob':
+ case 'varbinary':
+ if (base64_decode($value, true) === false) {
+ return 'invalid base64';
+ }
+ if ($column->hasLength() && strlen(base64_decode($value)) > $column->getLength()) {
+ return 'string too long';
+ }
+ break;
+ case 'geometry':
+ // no checks yet
+ break;
+ }
+ } else { // check non-string types
+ switch ($column->getType()) {
+ case 'integer':
+ case 'bigint':
+ if (!is_int($value)) {
+ return 'invalid integer';
+ }
+ break;
+ case 'float':
+ case 'double':
+ if (!is_float($value) && !is_int($value)) {
+ return 'invalid float';
+ }
+ break;
+ case 'boolean':
+ if (!is_bool($value) && ($value !== 0) && ($value !== 1)) {
+ return 'invalid boolean';
+ }
+ break;
+ default:
+ return 'invalid ' . $column->getType();
+ }
+ }
+ // extra checks
+ switch ($column->getType()) {
+ case 'integer': // 4 byte signed
+ $value = filter_var($value, FILTER_VALIDATE_INT);
+ if ($value > 2147483647 || $value < -2147483648) {
+ return 'invalid integer';
+ }
+ break;
+ }
+ }
+ return (true);
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $operation = RequestUtils::getOperation($request);
+ if (in_array($operation, ['create', 'update', 'increment'])) {
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ if ($this->reflection->hasTable($tableName)) {
+ $record = $request->getParsedBody();
+ if ($record !== null) {
+ $handler = $this->getProperty('handler', '');
+ if ($handler !== '') {
+ $table = $this->reflection->getTable($tableName);
+ if (is_array($record)) {
+ foreach ($record as $r) {
+ $response = $this->callHandler($handler, $r, $operation, $table);
+ if ($response !== null) {
+ return $response;
+ }
+ }
+ } else {
+ $response = $this->callHandler($handler, $record, $operation, $table);
+ if ($response !== null) {
+ return $response;
+ }
+ }
+ }
+ }
+ }
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/XmlMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Middleware\Router\Router;
+ use Tqdev\PhpCrudApi\RequestUtils;
+ use Tqdev\PhpCrudApi\ResponseFactory;
+
+ class XmlMiddleware extends Middleware
+ {
+ private $reflection;
+
+ public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
+ {
+ parent::__construct($router, $responder, $properties);
+ $this->reflection = $reflection;
+ }
+
+ private function json2xml($json, $types = 'null,boolean,number,string,object,array')
+ {
+ $a = json_decode($json);
+ $d = new \DOMDocument();
+ $c = $d->createElement("root");
+ $d->appendChild($c);
+ $t = function ($v) {
+ $type = gettype($v);
+ switch ($type) {
+ case 'integer':
+ return 'number';
+ case 'double':
+ return 'number';
+ default:
+ return strtolower($type);
+ }
+ };
+ $ts = explode(',', $types);
+ $f = function ($f, $c, $a, $s = false) use ($t, $d, $ts) {
+ if (in_array($t($a), $ts)) {
+ $c->setAttribute('type', $t($a));
+ }
+ if ($t($a) != 'array' && $t($a) != 'object') {
+ if ($t($a) == 'boolean') {
+ $c->appendChild($d->createTextNode($a ? 'true' : 'false'));
+ } else {
+ $c->appendChild($d->createTextNode($a));
+ }
+ } else {
+ foreach ($a as $k => $v) {
+ if ($k == '__type' && $t($a) == 'object') {
+ $c->setAttribute('__type', $v);
+ } else {
+ if ($t($v) == 'object') {
+ $ch = $c->appendChild($d->createElementNS(null, $s ? 'item' : $k));
+ $f($f, $ch, $v);
+ } else if ($t($v) == 'array') {
+ $ch = $c->appendChild($d->createElementNS(null, $s ? 'item' : $k));
+ $f($f, $ch, $v, true);
+ } else {
+ $va = $d->createElementNS(null, $s ? 'item' : $k);
+ if ($t($v) == 'boolean') {
+ $va->appendChild($d->createTextNode($v ? 'true' : 'false'));
+ } else {
+ $va->appendChild($d->createTextNode($v));
+ }
+ $ch = $c->appendChild($va);
+ if (in_array($t($v), $ts)) {
+ $ch->setAttribute('type', $t($v));
+ }
+ }
+ }
+ }
+ }
+ };
+ $f($f, $c, $a, $t($a) == 'array');
+ return $d->saveXML($d->documentElement);
+ }
+
+ private function xml2json($xml)
+ {
+ $a = @dom_import_simplexml(simplexml_load_string($xml));
+ if (!$a) {
+ return null;
+ }
+ $t = function ($v) {
+ $t = $v->getAttribute('type');
+ $txt = $v->firstChild->nodeType == XML_TEXT_NODE;
+ return $t ?: ($txt ? 'string' : 'object');
+ };
+ $f = function ($f, $a) use ($t) {
+ $c = null;
+ if ($t($a) == 'null') {
+ $c = null;
+ } else if ($t($a) == 'boolean') {
+ $b = substr(strtolower($a->textContent), 0, 1);
+ $c = in_array($b, array('1', 't'));
+ } else if ($t($a) == 'number') {
+ $c = $a->textContent + 0;
+ } else if ($t($a) == 'string') {
+ $c = $a->textContent;
+ } else if ($t($a) == 'object') {
+ $c = array();
+ if ($a->getAttribute('__type')) {
+ $c['__type'] = $a->getAttribute('__type');
+ }
+ for ($i = 0; $i < $a->childNodes->length; $i++) {
+ $v = $a->childNodes[$i];
+ $c[$v->nodeName] = $f($f, $v);
+ }
+ $c = (object) $c;
+ } else if ($t($a) == 'array') {
+ $c = array();
+ for ($i = 0; $i < $a->childNodes->length; $i++) {
+ $v = $a->childNodes[$i];
+ $c[$i] = $f($f, $v);
+ }
+ }
+ return $c;
+ };
+ $c = $f($f, $a);
+ return json_encode($c);
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ parse_str($request->getUri()->getQuery(), $params);
+ $isXml = isset($params['format']) && $params['format'] == 'xml';
+ if ($isXml) {
+ $body = $request->getBody()->getContents();
+ if ($body) {
+ $json = $this->xml2json($body);
+ $request = $request->withParsedBody(json_decode($json));
+ }
+ }
+ $response = $next->handle($request);
+ if ($isXml) {
+ $body = $response->getBody()->getContents();
+ if ($body) {
+ $types = implode(',', $this->getArrayProperty('types', 'null,array'));
+ if ($types == '' || $types == 'all') {
+ $xml = $this->json2xml($body);
+ } else {
+ $xml = $this->json2xml($body, $types);
+ }
+ $response = ResponseFactory::fromXml(ResponseFactory::OK, $xml);
+ }
+ }
+ return $response;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Middleware/XsrfMiddleware.php
+namespace Tqdev\PhpCrudApi\Middleware {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Controller\Responder;
+ use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+
+ class XsrfMiddleware extends Middleware
+ {
+ private function getToken(): string
+ {
+ $cookieName = $this->getProperty('cookieName', 'XSRF-TOKEN');
+ if (isset($_COOKIE[$cookieName])) {
+ $token = $_COOKIE[$cookieName];
+ } else {
+ $secure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on';
+ $token = bin2hex(random_bytes(8));
+ if (!headers_sent()) {
+ setcookie($cookieName, $token, 0, '', '', $secure);
+ }
+ }
+ return $token;
+ }
+
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
+ {
+ $token = $this->getToken();
+ $method = $request->getMethod();
+ $excludeMethods = $this->getArrayProperty('excludeMethods', 'OPTIONS,GET');
+ if (!in_array($method, $excludeMethods)) {
+ $headerName = $this->getProperty('headerName', 'X-XSRF-TOKEN');
+ if ($token != $request->getHeader($headerName)) {
+ return $this->responder->error(ErrorCode::BAD_OR_MISSING_XSRF_TOKEN, '');
+ }
+ }
+ return $next->handle($request);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/OpenApi/OpenApiBuilder.php
+namespace Tqdev\PhpCrudApi\OpenApi {
+
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\OpenApi\OpenApiDefinition;
+
+ class OpenApiBuilder
+ {
+ private $openapi;
+ private $records;
+ private $columns;
+ private $builders;
+
+ public function __construct(ReflectionService $reflection, array $base, array $controllers, array $builders)
+ {
+ $this->openapi = new OpenApiDefinition($base);
+ $this->records = in_array('records', $controllers) ? new OpenApiRecordsBuilder($this->openapi, $reflection) : null;
+ $this->columns = in_array('columns', $controllers) ? new OpenApiColumnsBuilder($this->openapi) : null;
+ $this->builders = array();
+ foreach ($builders as $className) {
+ $this->builders[] = new $className($this->openapi, $reflection);
+ }
+ }
+
+ private function getServerUrl(): string
+ {
+ $protocol = @$_SERVER['HTTP_X_FORWARDED_PROTO'] ?: @$_SERVER['REQUEST_SCHEME'] ?: ((isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] == "on") ? "https" : "http");
+ $port = @intval($_SERVER['HTTP_X_FORWARDED_PORT']) ?: @intval($_SERVER["SERVER_PORT"]) ?: (($protocol === 'https') ? 443 : 80);
+ $host = @explode(":", $_SERVER['HTTP_HOST'])[0] ?: @$_SERVER['SERVER_NAME'] ?: @$_SERVER['SERVER_ADDR'];
+ $port = ($protocol === 'https' && $port === 443) || ($protocol === 'http' && $port === 80) ? '' : ':' . $port;
+ $path = @trim(substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], '/openapi')), '/');
+ return sprintf('%s://%s%s/%s', $protocol, $host, $port, $path);
+ }
+
+ public function build(): OpenApiDefinition
+ {
+ $this->openapi->set("openapi", "3.0.0");
+ if (!$this->openapi->has("servers") && isset($_SERVER['REQUEST_URI'])) {
+ $this->openapi->set("servers|0|url", $this->getServerUrl());
+ }
+ if ($this->records) {
+ $this->records->build();
+ }
+ if ($this->columns) {
+ $this->columns->build();
+ }
+ foreach ($this->builders as $builder) {
+ $builder->build();
+ }
+ return $this->openapi;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/OpenApi/OpenApiColumnsBuilder.php
+namespace Tqdev\PhpCrudApi\OpenApi {
+
+ use Tqdev\PhpCrudApi\OpenApi\OpenApiDefinition;
+
+ class OpenApiColumnsBuilder
+ {
+ private $openapi;
+ private $operations = [
+ 'database' => [
+ 'read' => 'get',
+ ],
+ 'table' => [
+ 'create' => 'post',
+ 'read' => 'get',
+ 'update' => 'put', //rename
+ 'delete' => 'delete',
+ ],
+ 'column' => [
+ 'create' => 'post',
+ 'read' => 'get',
+ 'update' => 'put',
+ 'delete' => 'delete',
+ ]
+ ];
+
+ public function __construct(OpenApiDefinition $openapi)
+ {
+ $this->openapi = $openapi;
+ }
+
+ public function build() /*: void*/
+ {
+ $this->setPaths();
+ $this->openapi->set("components|responses|boolSuccess|description", "boolean indicating success or failure");
+ $this->openapi->set("components|responses|boolSuccess|content|application/json|schema|type", "boolean");
+ $this->setComponentSchema();
+ $this->setComponentResponse();
+ $this->setComponentRequestBody();
+ $this->setComponentParameters();
+ foreach (array_keys($this->operations) as $index => $type) {
+ $this->setTag($index, $type);
+ }
+ }
+
+ private function setPaths() /*: void*/
+ {
+ foreach (array_keys($this->operations) as $type) {
+ foreach ($this->operations[$type] as $operation => $method) {
+ $parameters = [];
+ switch ($type) {
+ case 'database':
+ $path = '/columns';
+ break;
+ case 'table':
+ $path = $operation == 'create' ? '/columns' : '/columns/{table}';
+ break;
+ case 'column':
+ $path = $operation == 'create' ? '/columns/{table}' : '/columns/{table}/{column}';
+ break;
+ }
+ if (strpos($path, '{table}')) {
+ $parameters[] = 'table';
+ }
+ if (strpos($path, '{column}')) {
+ $parameters[] = 'column';
+ }
+ foreach ($parameters as $p => $parameter) {
+ $this->openapi->set("paths|$path|$method|parameters|$p|\$ref", "#/components/parameters/$parameter");
+ }
+ $operationType = $operation . ucfirst($type);
+ if (in_array($operation, ['create', 'update'])) {
+ $this->openapi->set("paths|$path|$method|requestBody|\$ref", "#/components/requestBodies/$operationType");
+ }
+ $this->openapi->set("paths|$path|$method|tags|0", "$type");
+ $this->openapi->set("paths|$path|$method|operationId", "$operation" . "_" . "$type");
+ if ($operationType == 'updateTable') {
+ $this->openapi->set("paths|$path|$method|description", "rename table");
+ } else {
+ $this->openapi->set("paths|$path|$method|description", "$operation $type");
+ }
+ switch ($operation) {
+ case 'read':
+ $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/$operationType");
+ break;
+ case 'create':
+ case 'update':
+ case 'delete':
+ $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/boolSuccess");
+ break;
+ }
+ }
+ }
+ }
+
+ private function setComponentSchema() /*: void*/
+ {
+ foreach (array_keys($this->operations) as $type) {
+ foreach (array_keys($this->operations[$type]) as $operation) {
+ if ($operation == 'delete') {
+ continue;
+ }
+ $operationType = $operation . ucfirst($type);
+ $prefix = "components|schemas|$operationType";
+ $this->openapi->set("$prefix|type", "object");
+ switch ($type) {
+ case 'database':
+ $this->openapi->set("$prefix|properties|tables|type", 'array');
+ $this->openapi->set("$prefix|properties|tables|items|\$ref", "#/components/schemas/readTable");
+ break;
+ case 'table':
+ if ($operation == 'update') {
+ $this->openapi->set("$prefix|required", ['name']);
+ $this->openapi->set("$prefix|properties|name|type", 'string');
+ } else {
+ $this->openapi->set("$prefix|properties|name|type", 'string');
+ if ($operation == 'read') {
+ $this->openapi->set("$prefix|properties|type|type", 'string');
+ }
+ $this->openapi->set("$prefix|properties|columns|type", 'array');
+ $this->openapi->set("$prefix|properties|columns|items|\$ref", "#/components/schemas/readColumn");
+ }
+ break;
+ case 'column':
+ $this->openapi->set("$prefix|required", ['name', 'type']);
+ $this->openapi->set("$prefix|properties|name|type", 'string');
+ $this->openapi->set("$prefix|properties|type|type", 'string');
+ $this->openapi->set("$prefix|properties|length|type", 'integer');
+ $this->openapi->set("$prefix|properties|length|format", "int64");
+ $this->openapi->set("$prefix|properties|precision|type", 'integer');
+ $this->openapi->set("$prefix|properties|precision|format", "int64");
+ $this->openapi->set("$prefix|properties|scale|type", 'integer');
+ $this->openapi->set("$prefix|properties|scale|format", "int64");
+ $this->openapi->set("$prefix|properties|nullable|type", 'boolean');
+ $this->openapi->set("$prefix|properties|pk|type", 'boolean');
+ $this->openapi->set("$prefix|properties|fk|type", 'string');
+ break;
+ }
+ }
+ }
+ }
+
+ private function setComponentResponse() /*: void*/
+ {
+ foreach (array_keys($this->operations) as $type) {
+ foreach (array_keys($this->operations[$type]) as $operation) {
+ if ($operation != 'read') {
+ continue;
+ }
+ $operationType = $operation . ucfirst($type);
+ $this->openapi->set("components|responses|$operationType|description", "single $type record");
+ $this->openapi->set("components|responses|$operationType|content|application/json|schema|\$ref", "#/components/schemas/$operationType");
+ }
+ }
+ }
+
+ private function setComponentRequestBody() /*: void*/
+ {
+ foreach (array_keys($this->operations) as $type) {
+ foreach (array_keys($this->operations[$type]) as $operation) {
+ if (!in_array($operation, ['create', 'update'])) {
+ continue;
+ }
+ $operationType = $operation . ucfirst($type);
+ $this->openapi->set("components|requestBodies|$operationType|description", "single $type record");
+ $this->openapi->set("components|requestBodies|$operationType|content|application/json|schema|\$ref", "#/components/schemas/$operationType");
+ }
+ }
+ }
+
+ private function setComponentParameters() /*: void*/
+ {
+ $this->openapi->set("components|parameters|table|name", "table");
+ $this->openapi->set("components|parameters|table|in", "path");
+ $this->openapi->set("components|parameters|table|schema|type", "string");
+ $this->openapi->set("components|parameters|table|description", "table name");
+ $this->openapi->set("components|parameters|table|required", true);
+
+ $this->openapi->set("components|parameters|column|name", "column");
+ $this->openapi->set("components|parameters|column|in", "path");
+ $this->openapi->set("components|parameters|column|schema|type", "string");
+ $this->openapi->set("components|parameters|column|description", "column name");
+ $this->openapi->set("components|parameters|column|required", true);
+ }
+
+ private function setTag(int $index, string $type) /*: void*/
+ {
+ $this->openapi->set("tags|$index|name", "$type");
+ $this->openapi->set("tags|$index|description", "$type operations");
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/OpenApi/OpenApiDefinition.php
+namespace Tqdev\PhpCrudApi\OpenApi {
+
+ class OpenApiDefinition implements \JsonSerializable
+ {
+ private $root;
+
+ public function __construct(array $base)
+ {
+ $this->root = $base;
+ }
+
+ public function set(string $path, $value) /*: void*/
+ {
+ $parts = explode('|', trim($path, '|'));
+ $current = &$this->root;
+ while (count($parts) > 0) {
+ $part = array_shift($parts);
+ if (!isset($current[$part])) {
+ $current[$part] = [];
+ }
+ $current = &$current[$part];
+ }
+ $current = $value;
+ }
+
+ public function has(string $path): bool
+ {
+ $parts = explode('|', trim($path, '|'));
+ $current = &$this->root;
+ while (count($parts) > 0) {
+ $part = array_shift($parts);
+ if (!isset($current[$part])) {
+ return false;
+ }
+ $current = &$current[$part];
+ }
+ return true;
+ }
+
+ public function jsonSerialize()
+ {
+ return $this->root;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/OpenApi/OpenApiRecordsBuilder.php
+namespace Tqdev\PhpCrudApi\OpenApi {
+
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
+ use Tqdev\PhpCrudApi\Middleware\Communication\VariableStore;
+ use Tqdev\PhpCrudApi\OpenApi\OpenApiDefinition;
+
+ class OpenApiRecordsBuilder
+ {
+ private $openapi;
+ private $reflection;
+ private $operations = [
+ 'list' => 'get',
+ 'create' => 'post',
+ 'read' => 'get',
+ 'update' => 'put',
+ 'delete' => 'delete',
+ 'increment' => 'patch',
+ ];
+ private $types = [
+ 'integer' => ['type' => 'integer', 'format' => 'int32'],
+ 'bigint' => ['type' => 'integer', 'format' => 'int64'],
+ 'varchar' => ['type' => 'string'],
+ 'clob' => ['type' => 'string', 'format' => 'large-string'], //custom format
+ 'varbinary' => ['type' => 'string', 'format' => 'byte'],
+ 'blob' => ['type' => 'string', 'format' => 'large-byte'], //custom format
+ 'decimal' => ['type' => 'string', 'format' => 'decimal'], //custom format
+ 'float' => ['type' => 'number', 'format' => 'float'],
+ 'double' => ['type' => 'number', 'format' => 'double'],
+ 'date' => ['type' => 'string', 'format' => 'date'],
+ 'time' => ['type' => 'string', 'format' => 'time'], //custom format
+ 'timestamp' => ['type' => 'string', 'format' => 'date-time'],
+ 'geometry' => ['type' => 'string', 'format' => 'geometry'], //custom format
+ 'boolean' => ['type' => 'boolean'],
+ ];
+
+ public function __construct(OpenApiDefinition $openapi, ReflectionService $reflection)
+ {
+ $this->openapi = $openapi;
+ $this->reflection = $reflection;
+ }
+
+ private function getAllTableReferences(): array
+ {
+ $tableReferences = array();
+ foreach ($this->reflection->getTableNames() as $tableName) {
+ $table = $this->reflection->getTable($tableName);
+ foreach ($table->getColumnNames() as $columnName) {
+ $column = $table->getColumn($columnName);
+ $referencedTableName = $column->getFk();
+ if ($referencedTableName) {
+ if (!isset($tableReferences[$referencedTableName])) {
+ $tableReferences[$referencedTableName] = array();
+ }
+ $tableReferences[$referencedTableName][] = "$tableName.$columnName";
+ }
+ }
+ }
+ return $tableReferences;
+ }
+
+ public function build() /*: void*/
+ {
+ $tableNames = $this->reflection->getTableNames();
+ foreach ($tableNames as $tableName) {
+ $this->setPath($tableName);
+ }
+ $this->openapi->set("components|responses|pk_integer|description", "inserted primary key value (integer)");
+ $this->openapi->set("components|responses|pk_integer|content|application/json|schema|type", "integer");
+ $this->openapi->set("components|responses|pk_integer|content|application/json|schema|format", "int64");
+ $this->openapi->set("components|responses|pk_string|description", "inserted primary key value (string)");
+ $this->openapi->set("components|responses|pk_string|content|application/json|schema|type", "string");
+ $this->openapi->set("components|responses|pk_string|content|application/json|schema|format", "uuid");
+ $this->openapi->set("components|responses|rows_affected|description", "number of rows affected (integer)");
+ $this->openapi->set("components|responses|rows_affected|content|application/json|schema|type", "integer");
+ $this->openapi->set("components|responses|rows_affected|content|application/json|schema|format", "int64");
+ $tableReferences = $this->getAllTableReferences();
+ foreach ($tableNames as $tableName) {
+ $references = isset($tableReferences[$tableName]) ? $tableReferences[$tableName] : array();
+ $this->setComponentSchema($tableName, $references);
+ $this->setComponentResponse($tableName);
+ $this->setComponentRequestBody($tableName);
+ }
+ $this->setComponentParameters();
+ foreach ($tableNames as $index => $tableName) {
+ $this->setTag($index, $tableName);
+ }
+ }
+
+ private function isOperationOnTableAllowed(string $operation, string $tableName): bool
+ {
+ $tableHandler = VariableStore::get('authorization.tableHandler');
+ if (!$tableHandler) {
+ return true;
+ }
+ return (bool) call_user_func($tableHandler, $operation, $tableName);
+ }
+
+ private function isOperationOnColumnAllowed(string $operation, string $tableName, string $columnName): bool
+ {
+ $columnHandler = VariableStore::get('authorization.columnHandler');
+ if (!$columnHandler) {
+ return true;
+ }
+ return (bool) call_user_func($columnHandler, $operation, $tableName, $columnName);
+ }
+
+ private function setPath(string $tableName) /*: void*/
+ {
+ $table = $this->reflection->getTable($tableName);
+ $type = $table->getType();
+ $pk = $table->getPk();
+ $pkName = $pk ? $pk->getName() : '';
+ foreach ($this->operations as $operation => $method) {
+ if (!$pkName && $operation != 'list') {
+ continue;
+ }
+ if ($type != 'table' && $operation != 'list') {
+ continue;
+ }
+ if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
+ continue;
+ }
+ $parameters = [];
+ if (in_array($operation, ['list', 'create'])) {
+ $path = sprintf('/records/%s', $tableName);
+ if ($operation == 'list') {
+ $parameters = ['filter', 'include', 'exclude', 'order', 'size', 'page', 'join'];
+ }
+ } else {
+ $path = sprintf('/records/%s/{id}', $tableName);
+ if ($operation == 'read') {
+ $parameters = ['pk', 'include', 'exclude', 'join'];
+ } else {
+ $parameters = ['pk'];
+ }
+ }
+ foreach ($parameters as $p => $parameter) {
+ $this->openapi->set("paths|$path|$method|parameters|$p|\$ref", "#/components/parameters/$parameter");
+ }
+ if (in_array($operation, ['create', 'update', 'increment'])) {
+ $this->openapi->set("paths|$path|$method|requestBody|\$ref", "#/components/requestBodies/$operation-" . rawurlencode($tableName));
+ }
+ $this->openapi->set("paths|$path|$method|tags|0", "$tableName");
+ $this->openapi->set("paths|$path|$method|operationId", "$operation" . "_" . "$tableName");
+ $this->openapi->set("paths|$path|$method|description", "$operation $tableName");
+ switch ($operation) {
+ case 'list':
+ $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/$operation-" . rawurlencode($tableName));
+ break;
+ case 'create':
+ if ($pk->getType() == 'integer') {
+ $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/pk_integer");
+ } else {
+ $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/pk_string");
+ }
+ break;
+ case 'read':
+ $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/$operation-" . rawurlencode($tableName));
+ break;
+ case 'update':
+ case 'delete':
+ case 'increment':
+ $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/rows_affected");
+ break;
+ }
+ }
+ }
+
+ private function getPattern(ReflectedColumn $column): string
+ {
+ switch ($column->getType()) {
+ case 'integer':
+ $n = strlen(pow(2, 31));
+ return '^-?[0-9]{1,' . $n . '}$';
+ case 'bigint':
+ $n = strlen(pow(2, 63));
+ return '^-?[0-9]{1,' . $n . '}$';
+ case 'varchar':
+ $l = $column->getLength();
+ return '^.{0,' . $l . '}$';
+ case 'clob':
+ return '^.*$';
+ case 'varbinary':
+ $l = $column->getLength();
+ $b = (int) 4 * ceil($l / 3);
+ return '^[A-Za-z0-9+/]{0,' . $b . '}=*$';
+ case 'blob':
+ return '^[A-Za-z0-9+/]*=*$';
+ case 'decimal':
+ $p = $column->getPrecision();
+ $s = $column->getScale();
+ return '^-?[0-9]{1,' . ($p - $s) . '}(\.[0-9]{1,' . $s . '})?$';
+ case 'float':
+ return '^-?[0-9]+(\.[0-9]+)?([eE]-?[0-9]+)?$';
+ case 'double':
+ return '^-?[0-9]+(\.[0-9]+)?([eE]-?[0-9]+)?$';
+ case 'date':
+ return '^[0-9]{4}-[0-9]{2}-[0-9]{2}$';
+ case 'time':
+ return '^[0-9]{2}:[0-9]{2}:[0-9]{2}$';
+ case 'timestamp':
+ return '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$';
+ return '';
+ case 'geometry':
+ return '^(POINT|LINESTRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON)\s*\(.*$';
+ case 'boolean':
+ return '^(true|false)$';
+ }
+ return '';
+ }
+
+ private function setComponentSchema(string $tableName, array $references) /*: void*/
+ {
+ $table = $this->reflection->getTable($tableName);
+ $type = $table->getType();
+ $pk = $table->getPk();
+ $pkName = $pk ? $pk->getName() : '';
+ foreach ($this->operations as $operation => $method) {
+ if (!$pkName && $operation != 'list') {
+ continue;
+ }
+ if ($type == 'view' && !in_array($operation, array('read', 'list'))) {
+ continue;
+ }
+ if ($type == 'view' && !$pkName && $operation == 'read') {
+ continue;
+ }
+ if ($operation == 'delete') {
+ continue;
+ }
+ if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
+ continue;
+ }
+ if ($operation == 'list') {
+ $this->openapi->set("components|schemas|$operation-$tableName|type", "object");
+ $this->openapi->set("components|schemas|$operation-$tableName|properties|results|type", "integer");
+ $this->openapi->set("components|schemas|$operation-$tableName|properties|results|format", "int64");
+ $this->openapi->set("components|schemas|$operation-$tableName|properties|records|type", "array");
+ $prefix = "components|schemas|$operation-$tableName|properties|records|items";
+ } else {
+ $prefix = "components|schemas|$operation-$tableName";
+ }
+ $this->openapi->set("$prefix|type", "object");
+ foreach ($table->getColumnNames() as $columnName) {
+ if (!$this->isOperationOnColumnAllowed($operation, $tableName, $columnName)) {
+ continue;
+ }
+ $column = $table->getColumn($columnName);
+ $properties = $this->types[$column->getType()];
+ $properties['maxLength'] = $column->hasLength() ? $column->getLength() : 0;
+ $properties['nullable'] = $column->getNullable();
+ $properties['pattern'] = $this->getPattern($column);
+ foreach ($properties as $key => $value) {
+ if ($value) {
+ $this->openapi->set("$prefix|properties|$columnName|$key", $value);
+ }
+ }
+ if ($column->getPk()) {
+ $this->openapi->set("$prefix|properties|$columnName|x-primary-key", true);
+ $this->openapi->set("$prefix|properties|$columnName|x-referenced", $references);
+ }
+ $fk = $column->getFk();
+ if ($fk) {
+ $this->openapi->set("$prefix|properties|$columnName|x-references", $fk);
+ }
+ }
+ }
+ }
+
+ private function setComponentResponse(string $tableName) /*: void*/
+ {
+ $table = $this->reflection->getTable($tableName);
+ $type = $table->getType();
+ $pk = $table->getPk();
+ $pkName = $pk ? $pk->getName() : '';
+ foreach (['list', 'read'] as $operation) {
+ if (!$pkName && $operation != 'list') {
+ continue;
+ }
+ if ($type != 'table' && $operation != 'list') {
+ continue;
+ }
+ if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
+ continue;
+ }
+ if ($operation == 'list') {
+ $this->openapi->set("components|responses|$operation-$tableName|description", "list of $tableName records");
+ } else {
+ $this->openapi->set("components|responses|$operation-$tableName|description", "single $tableName record");
+ }
+ $this->openapi->set("components|responses|$operation-$tableName|content|application/json|schema|\$ref", "#/components/schemas/$operation-" . rawurlencode($tableName));
+ }
+ }
+
+ private function setComponentRequestBody(string $tableName) /*: void*/
+ {
+ $table = $this->reflection->getTable($tableName);
+ $type = $table->getType();
+ $pk = $table->getPk();
+ $pkName = $pk ? $pk->getName() : '';
+ if ($pkName && $type == 'table') {
+ foreach (['create', 'update', 'increment'] as $operation) {
+ if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
+ continue;
+ }
+ $this->openapi->set("components|requestBodies|$operation-$tableName|description", "single $tableName record");
+ $this->openapi->set("components|requestBodies|$operation-$tableName|content|application/json|schema|\$ref", "#/components/schemas/$operation-" . rawurlencode($tableName));
+ }
+ }
+ }
+
+ private function setComponentParameters() /*: void*/
+ {
+ $this->openapi->set("components|parameters|pk|name", "id");
+ $this->openapi->set("components|parameters|pk|in", "path");
+ $this->openapi->set("components|parameters|pk|schema|type", "string");
+ $this->openapi->set("components|parameters|pk|description", "primary key value");
+ $this->openapi->set("components|parameters|pk|required", true);
+
+ $this->openapi->set("components|parameters|filter|name", "filter");
+ $this->openapi->set("components|parameters|filter|in", "query");
+ $this->openapi->set("components|parameters|filter|schema|type", "array");
+ $this->openapi->set("components|parameters|filter|schema|items|type", "string");
+ $this->openapi->set("components|parameters|filter|description", "Filters to be applied. Each filter consists of a column, an operator and a value (comma separated). Example: id,eq,1");
+ $this->openapi->set("components|parameters|filter|required", false);
+
+ $this->openapi->set("components|parameters|include|name", "include");
+ $this->openapi->set("components|parameters|include|in", "query");
+ $this->openapi->set("components|parameters|include|schema|type", "string");
+ $this->openapi->set("components|parameters|include|description", "Columns you want to include in the output (comma separated). Example: posts.*,categories.name");
+ $this->openapi->set("components|parameters|include|required", false);
+
+ $this->openapi->set("components|parameters|exclude|name", "exclude");
+ $this->openapi->set("components|parameters|exclude|in", "query");
+ $this->openapi->set("components|parameters|exclude|schema|type", "string");
+ $this->openapi->set("components|parameters|exclude|description", "Columns you want to exclude from the output (comma separated). Example: posts.content");
+ $this->openapi->set("components|parameters|exclude|required", false);
+
+ $this->openapi->set("components|parameters|order|name", "order");
+ $this->openapi->set("components|parameters|order|in", "query");
+ $this->openapi->set("components|parameters|order|schema|type", "array");
+ $this->openapi->set("components|parameters|order|schema|items|type", "string");
+ $this->openapi->set("components|parameters|order|description", "Column you want to sort on and the sort direction (comma separated). Example: id,desc");
+ $this->openapi->set("components|parameters|order|required", false);
+
+ $this->openapi->set("components|parameters|size|name", "size");
+ $this->openapi->set("components|parameters|size|in", "query");
+ $this->openapi->set("components|parameters|size|schema|type", "string");
+ $this->openapi->set("components|parameters|size|description", "Maximum number of results (for top lists). Example: 10");
+ $this->openapi->set("components|parameters|size|required", false);
+
+ $this->openapi->set("components|parameters|page|name", "page");
+ $this->openapi->set("components|parameters|page|in", "query");
+ $this->openapi->set("components|parameters|page|schema|type", "string");
+ $this->openapi->set("components|parameters|page|description", "Page number and page size (comma separated). Example: 1,10");
+ $this->openapi->set("components|parameters|page|required", false);
+
+ $this->openapi->set("components|parameters|join|name", "join");
+ $this->openapi->set("components|parameters|join|in", "query");
+ $this->openapi->set("components|parameters|join|schema|type", "array");
+ $this->openapi->set("components|parameters|join|schema|items|type", "string");
+ $this->openapi->set("components|parameters|join|description", "Paths (comma separated) to related entities that you want to include. Example: comments,users");
+ $this->openapi->set("components|parameters|join|required", false);
+ }
+
+ private function setTag(int $index, string $tableName) /*: void*/
+ {
+ $this->openapi->set("tags|$index|name", "$tableName");
+ $this->openapi->set("tags|$index|description", "$tableName operations");
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/OpenApi/OpenApiService.php
+namespace Tqdev\PhpCrudApi\OpenApi {
+
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\OpenApi\OpenApiBuilder;
+
+ class OpenApiService
+ {
+ private $builder;
+
+ public function __construct(ReflectionService $reflection, array $base, array $controllers, array $customBuilders)
+ {
+ $this->builder = new OpenApiBuilder($reflection, $base, $controllers, $customBuilders);
+ }
+
+ public function get(): OpenApiDefinition
+ {
+ return $this->builder->build();
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/Condition/AndCondition.php
+namespace Tqdev\PhpCrudApi\Record\Condition {
+
+ class AndCondition extends Condition
+ {
+ private $conditions;
+
+ public function __construct(Condition $condition1, Condition $condition2)
+ {
+ $this->conditions = [$condition1, $condition2];
+ }
+
+ public function _and(Condition $condition): Condition
+ {
+ if ($condition instanceof NoCondition) {
+ return $this;
+ }
+ $this->conditions[] = $condition;
+ return $this;
+ }
+
+ public function getConditions(): array
+ {
+ return $this->conditions;
+ }
+
+ public static function fromArray(array $conditions): Condition
+ {
+ $condition = new NoCondition();
+ foreach ($conditions as $c) {
+ $condition = $condition->_and($c);
+ }
+ return $condition;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/Condition/ColumnCondition.php
+namespace Tqdev\PhpCrudApi\Record\Condition {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
+
+ class ColumnCondition extends Condition
+ {
+ private $column;
+ private $operator;
+ private $value;
+
+ public function __construct(ReflectedColumn $column, string $operator, string $value)
+ {
+ $this->column = $column;
+ $this->operator = $operator;
+ $this->value = $value;
+ }
+
+ public function getColumn(): ReflectedColumn
+ {
+ return $this->column;
+ }
+
+ public function getOperator(): string
+ {
+ return $this->operator;
+ }
+
+ public function getValue(): string
+ {
+ return $this->value;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/Condition/Condition.php
+namespace Tqdev\PhpCrudApi\Record\Condition {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+
+ abstract class Condition
+ {
+ public function _and(Condition $condition): Condition
+ {
+ if ($condition instanceof NoCondition) {
+ return $this;
+ }
+ return new AndCondition($this, $condition);
+ }
+
+ public function _or(Condition $condition): Condition
+ {
+ if ($condition instanceof NoCondition) {
+ return $this;
+ }
+ return new OrCondition($this, $condition);
+ }
+
+ public function _not(): Condition
+ {
+ return new NotCondition($this);
+ }
+
+ public static function fromString(ReflectedTable $table, string $value): Condition
+ {
+ $condition = new NoCondition();
+ $parts = explode(',', $value, 3);
+ if (count($parts) < 2) {
+ return $condition;
+ }
+ if (count($parts) < 3) {
+ $parts[2] = '';
+ }
+ $field = $table->getColumn($parts[0]);
+ $command = $parts[1];
+ $negate = false;
+ $spatial = false;
+ if (strlen($command) > 2) {
+ if (substr($command, 0, 1) == 'n') {
+ $negate = true;
+ $command = substr($command, 1);
+ }
+ if (substr($command, 0, 1) == 's') {
+ $spatial = true;
+ $command = substr($command, 1);
+ }
+ }
+ if ($spatial) {
+ if (in_array($command, ['co', 'cr', 'di', 'eq', 'in', 'ov', 'to', 'wi', 'ic', 'is', 'iv'])) {
+ $condition = new SpatialCondition($field, $command, $parts[2]);
+ }
+ } else {
+ if (in_array($command, ['cs', 'sw', 'ew', 'eq', 'lt', 'le', 'ge', 'gt', 'bt', 'in', 'is'])) {
+ $condition = new ColumnCondition($field, $command, $parts[2]);
+ }
+ }
+ if ($negate) {
+ $condition = $condition->_not();
+ }
+ return $condition;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/Condition/NoCondition.php
+namespace Tqdev\PhpCrudApi\Record\Condition {
+
+ class NoCondition extends Condition
+ {
+ public function _and(Condition $condition): Condition
+ {
+ return $condition;
+ }
+
+ public function _or(Condition $condition): Condition
+ {
+ return $condition;
+ }
+
+ public function _not(): Condition
+ {
+ return $this;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/Condition/NotCondition.php
+namespace Tqdev\PhpCrudApi\Record\Condition {
+
+ class NotCondition extends Condition
+ {
+ private $condition;
+
+ public function __construct(Condition $condition)
+ {
+ $this->condition = $condition;
+ }
+
+ public function getCondition(): Condition
+ {
+ return $this->condition;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/Condition/OrCondition.php
+namespace Tqdev\PhpCrudApi\Record\Condition {
+
+ class OrCondition extends Condition
+ {
+ private $conditions;
+
+ public function __construct(Condition $condition1, Condition $condition2)
+ {
+ $this->conditions = [$condition1, $condition2];
+ }
+
+ public function _or(Condition $condition): Condition
+ {
+ if ($condition instanceof NoCondition) {
+ return $this;
+ }
+ $this->conditions[] = $condition;
+ return $this;
+ }
+
+ public function getConditions(): array
+ {
+ return $this->conditions;
+ }
+
+ public static function fromArray(array $conditions): Condition
+ {
+ $condition = new NoCondition();
+ foreach ($conditions as $c) {
+ $condition = $condition->_or($c);
+ }
+ return $condition;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/Condition/SpatialCondition.php
+namespace Tqdev\PhpCrudApi\Record\Condition {
+
+ class SpatialCondition extends ColumnCondition
+ {
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/Document/ErrorDocument.php
+namespace Tqdev\PhpCrudApi\Record\Document {
+
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+
+ class ErrorDocument implements \JsonSerializable
+ {
+ public $code;
+ public $message;
+ public $details;
+
+ public function __construct(ErrorCode $errorCode, string $argument, $details)
+ {
+ $this->code = $errorCode->getCode();
+ $this->message = $errorCode->getMessage($argument);
+ $this->details = $details;
+ }
+
+ public function getCode(): int
+ {
+ return $this->code;
+ }
+
+ public function getMessage(): string
+ {
+ return $this->message;
+ }
+
+ public function serialize()
+ {
+ return [
+ 'code' => $this->code,
+ 'message' => $this->message,
+ 'details' => $this->details,
+ ];
+ }
+
+ public function jsonSerialize()
+ {
+ return array_filter($this->serialize());
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/Document/ListDocument.php
+namespace Tqdev\PhpCrudApi\Record\Document {
+
+ class ListDocument implements \JsonSerializable
+ {
+ private $records;
+
+ private $results;
+
+ public function __construct(array $records, int $results)
+ {
+ $this->records = $records;
+ $this->results = $results;
+ }
+
+ public function getRecords(): array
+ {
+ return $this->records;
+ }
+
+ public function getResults(): int
+ {
+ return $this->results;
+ }
+
+ public function serialize()
+ {
+ return [
+ 'records' => $this->records,
+ 'results' => $this->results,
+ ];
+ }
+
+ public function jsonSerialize()
+ {
+ return array_filter($this->serialize(), function ($v) {
+ return $v !== 0;
+ });
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/ColumnIncluder.php
+namespace Tqdev\PhpCrudApi\Record {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+
+ class ColumnIncluder
+ {
+ private function isMandatory(string $tableName, string $columnName, array $params): bool
+ {
+ return isset($params['mandatory']) && in_array($tableName . "." . $columnName, $params['mandatory']);
+ }
+
+ private function select(
+ string $tableName,
+ bool $primaryTable,
+ array $params,
+ string $paramName,
+ array $columnNames,
+ bool $include
+ ): array {
+ if (!isset($params[$paramName])) {
+ return $columnNames;
+ }
+ $columns = array();
+ foreach (explode(',', $params[$paramName][0]) as $columnName) {
+ $columns[$columnName] = true;
+ }
+ $result = array();
+ foreach ($columnNames as $columnName) {
+ $match = isset($columns['*.*']);
+ if (!$match) {
+ $match = isset($columns[$tableName . '.*']) || isset($columns[$tableName . '.' . $columnName]);
+ }
+ if ($primaryTable && !$match) {
+ $match = isset($columns['*']) || isset($columns[$columnName]);
+ }
+ if ($match) {
+ if ($include || $this->isMandatory($tableName, $columnName, $params)) {
+ $result[] = $columnName;
+ }
+ } else {
+ if (!$include || $this->isMandatory($tableName, $columnName, $params)) {
+ $result[] = $columnName;
+ }
+ }
+ }
+ return $result;
+ }
+
+ public function getNames(ReflectedTable $table, bool $primaryTable, array $params): array
+ {
+ $tableName = $table->getName();
+ $results = $table->getColumnNames();
+ $results = $this->select($tableName, $primaryTable, $params, 'include', $results, true);
+ $results = $this->select($tableName, $primaryTable, $params, 'exclude', $results, false);
+ return $results;
+ }
+
+ public function getValues(ReflectedTable $table, bool $primaryTable, /* object */ $record, array $params): array
+ {
+ $results = array();
+ $columnNames = $this->getNames($table, $primaryTable, $params);
+ foreach ($columnNames as $columnName) {
+ if (property_exists($record, $columnName)) {
+ $results[$columnName] = $record->$columnName;
+ }
+ }
+ return $results;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/ErrorCode.php
+namespace Tqdev\PhpCrudApi\Record {
+
+ use Tqdev\PhpCrudApi\ResponseFactory;
+
+ class ErrorCode
+ {
+ private $code;
+ private $message;
+ private $status;
+
+ const ERROR_NOT_FOUND = 9999;
+ const ROUTE_NOT_FOUND = 1000;
+ const TABLE_NOT_FOUND = 1001;
+ const ARGUMENT_COUNT_MISMATCH = 1002;
+ const RECORD_NOT_FOUND = 1003;
+ const ORIGIN_FORBIDDEN = 1004;
+ const COLUMN_NOT_FOUND = 1005;
+ const TABLE_ALREADY_EXISTS = 1006;
+ const COLUMN_ALREADY_EXISTS = 1007;
+ const HTTP_MESSAGE_NOT_READABLE = 1008;
+ const DUPLICATE_KEY_EXCEPTION = 1009;
+ const DATA_INTEGRITY_VIOLATION = 1010;
+ const AUTHENTICATION_REQUIRED = 1011;
+ const AUTHENTICATION_FAILED = 1012;
+ const INPUT_VALIDATION_FAILED = 1013;
+ const OPERATION_FORBIDDEN = 1014;
+ const OPERATION_NOT_SUPPORTED = 1015;
+ const TEMPORARY_OR_PERMANENTLY_BLOCKED = 1016;
+ const BAD_OR_MISSING_XSRF_TOKEN = 1017;
+ const ONLY_AJAX_REQUESTS_ALLOWED = 1018;
+ const PAGINATION_FORBIDDEN = 1019;
+ const USER_ALREADY_EXIST = 1020;
+ const PASSWORD_TOO_SHORT = 1021;
+
+ private $values = [
+ 9999 => ["%s", ResponseFactory::INTERNAL_SERVER_ERROR],
+ 1000 => ["Route '%s' not found", ResponseFactory::NOT_FOUND],
+ 1001 => ["Table '%s' not found", ResponseFactory::NOT_FOUND],
+ 1002 => ["Argument count mismatch in '%s'", ResponseFactory::UNPROCESSABLE_ENTITY],
+ 1003 => ["Record '%s' not found", ResponseFactory::NOT_FOUND],
+ 1004 => ["Origin '%s' is forbidden", ResponseFactory::FORBIDDEN],
+ 1005 => ["Column '%s' not found", ResponseFactory::NOT_FOUND],
+ 1006 => ["Table '%s' already exists", ResponseFactory::CONFLICT],
+ 1007 => ["Column '%s' already exists", ResponseFactory::CONFLICT],
+ 1008 => ["Cannot read HTTP message", ResponseFactory::UNPROCESSABLE_ENTITY],
+ 1009 => ["Duplicate key exception", ResponseFactory::CONFLICT],
+ 1010 => ["Data integrity violation", ResponseFactory::CONFLICT],
+ 1011 => ["Authentication required", ResponseFactory::UNAUTHORIZED],
+ 1012 => ["Authentication failed for '%s'", ResponseFactory::FORBIDDEN],
+ 1013 => ["Input validation failed for '%s'", ResponseFactory::UNPROCESSABLE_ENTITY],
+ 1014 => ["Operation forbidden", ResponseFactory::FORBIDDEN],
+ 1015 => ["Operation '%s' not supported", ResponseFactory::METHOD_NOT_ALLOWED],
+ 1016 => ["Temporary or permanently blocked", ResponseFactory::FORBIDDEN],
+ 1017 => ["Bad or missing XSRF token", ResponseFactory::FORBIDDEN],
+ 1018 => ["Only AJAX requests allowed for '%s'", ResponseFactory::FORBIDDEN],
+ 1019 => ["Pagination forbidden", ResponseFactory::FORBIDDEN],
+ 1020 => ["User '%s' already exists", ResponseFactory::CONFLICT],
+ 1021 => ["Password too short (<%d characters)", ResponseFactory::UNPROCESSABLE_ENTITY],
+ ];
+
+ public function __construct(int $code)
+ {
+ if (!isset($this->values[$code])) {
+ $code = 9999;
+ }
+ $this->code = $code;
+ $this->message = $this->values[$code][0];
+ $this->status = $this->values[$code][1];
+ }
+
+ public function getCode(): int
+ {
+ return $this->code;
+ }
+
+ public function getMessage(string $argument): string
+ {
+ return sprintf($this->message, $argument);
+ }
+
+ public function getStatus(): int
+ {
+ return $this->status;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/FilterInfo.php
+namespace Tqdev\PhpCrudApi\Record {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+ use Tqdev\PhpCrudApi\Record\Condition\AndCondition;
+ use Tqdev\PhpCrudApi\Record\Condition\Condition;
+ use Tqdev\PhpCrudApi\Record\Condition\NoCondition;
+ use Tqdev\PhpCrudApi\Record\Condition\OrCondition;
+
+ class FilterInfo
+ {
+ private function getConditionsAsPathTree(ReflectedTable $table, array $params): PathTree
+ {
+ $conditions = new PathTree();
+ foreach ($params as $key => $filters) {
+ if (substr($key, 0, 6) == 'filter') {
+ preg_match_all('/\d+|\D+/', substr($key, 6), $matches);
+ $path = $matches[0];
+ foreach ($filters as $filter) {
+ $condition = Condition::fromString($table, $filter);
+ if (($condition instanceof NoCondition) == false) {
+ $conditions->put($path, $condition);
+ }
+ }
+ }
+ }
+ return $conditions;
+ }
+
+ private function combinePathTreeOfConditions(PathTree $tree): Condition
+ {
+ $andConditions = $tree->getValues();
+ $and = AndCondition::fromArray($andConditions);
+ $orConditions = [];
+ foreach ($tree->getKeys() as $p) {
+ $orConditions[] = $this->combinePathTreeOfConditions($tree->get($p));
+ }
+ $or = OrCondition::fromArray($orConditions);
+ return $and->_and($or);
+ }
+
+ public function getCombinedConditions(ReflectedTable $table, array $params): Condition
+ {
+ return $this->combinePathTreeOfConditions($this->getConditionsAsPathTree($table, $params));
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/HabtmValues.php
+namespace Tqdev\PhpCrudApi\Record {
+
+ class HabtmValues
+ {
+ public $pkValues;
+ public $fkValues;
+
+ public function __construct(array $pkValues, array $fkValues)
+ {
+ $this->pkValues = $pkValues;
+ $this->fkValues = $fkValues;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/OrderingInfo.php
+namespace Tqdev\PhpCrudApi\Record {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+
+ class OrderingInfo
+ {
+ public function getColumnOrdering(ReflectedTable $table, array $params): array
+ {
+ $fields = array();
+ if (isset($params['order'])) {
+ foreach ($params['order'] as $order) {
+ $parts = explode(',', $order, 3);
+ $columnName = $parts[0];
+ if (!$table->hasColumn($columnName)) {
+ continue;
+ }
+ $ascending = 'ASC';
+ if (count($parts) > 1) {
+ if (substr(strtoupper($parts[1]), 0, 4) == "DESC") {
+ $ascending = 'DESC';
+ }
+ }
+ $fields[] = [$columnName, $ascending];
+ }
+ }
+ if (count($fields) == 0) {
+ return $this->getDefaultColumnOrdering($table);
+ }
+ return $fields;
+ }
+
+ public function getDefaultColumnOrdering(ReflectedTable $table): array
+ {
+ $fields = array();
+ $pk = $table->getPk();
+ if ($pk) {
+ $fields[] = [$pk->getName(), 'ASC'];
+ } else {
+ foreach ($table->getColumnNames() as $columnName) {
+ $fields[] = [$columnName, 'ASC'];
+ }
+ }
+ return $fields;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/PaginationInfo.php
+namespace Tqdev\PhpCrudApi\Record {
+
+ class PaginationInfo
+ {
+ public $DEFAULT_PAGE_SIZE = 20;
+
+ public function hasPage(array $params): bool
+ {
+ return isset($params['page']);
+ }
+
+ public function getPageOffset(array $params): int
+ {
+ $offset = 0;
+ $pageSize = $this->getPageSize($params);
+ if (isset($params['page'])) {
+ foreach ($params['page'] as $page) {
+ $parts = explode(',', $page, 2);
+ $page = intval($parts[0]) - 1;
+ $offset = $page * $pageSize;
+ }
+ }
+ return $offset;
+ }
+
+ private function getPageSize(array $params): int
+ {
+ $pageSize = $this->DEFAULT_PAGE_SIZE;
+ if (isset($params['page'])) {
+ foreach ($params['page'] as $page) {
+ $parts = explode(',', $page, 2);
+ if (count($parts) > 1) {
+ $pageSize = intval($parts[1]);
+ }
+ }
+ }
+ return $pageSize;
+ }
+
+ public function getResultSize(array $params): int
+ {
+ $numberOfRows = -1;
+ if (isset($params['size'])) {
+ foreach ($params['size'] as $size) {
+ $numberOfRows = intval($size);
+ }
+ }
+ return $numberOfRows;
+ }
+
+ public function getPageLimit(array $params): int
+ {
+ $pageLimit = -1;
+ if ($this->hasPage($params)) {
+ $pageLimit = $this->getPageSize($params);
+ }
+ $resultSize = $this->getResultSize($params);
+ if ($resultSize >= 0) {
+ if ($pageLimit >= 0) {
+ $pageLimit = min($pageLimit, $resultSize);
+ } else {
+ $pageLimit = $resultSize;
+ }
+ }
+ return $pageLimit;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/PathTree.php
+namespace Tqdev\PhpCrudApi\Record {
+
+ class PathTree implements \JsonSerializable
+ {
+ const WILDCARD = '*';
+
+ private $tree;
+
+ public function __construct(/* object */&$tree = null)
+ {
+ if (!$tree) {
+ $tree = $this->newTree();
+ }
+ $this->tree = &$tree;
+ }
+
+ public function newTree()
+ {
+ return (object) ['values' => [], 'branches' => (object) []];
+ }
+
+ public function getKeys(): array
+ {
+ $branches = (array) $this->tree->branches;
+ return array_keys($branches);
+ }
+
+ public function getValues(): array
+ {
+ return $this->tree->values;
+ }
+
+ public function get(string $key): PathTree
+ {
+ if (!isset($this->tree->branches->$key)) {
+ return null;
+ }
+ return new PathTree($this->tree->branches->$key);
+ }
+
+ public function put(array $path, $value)
+ {
+ $tree = &$this->tree;
+ foreach ($path as $key) {
+ if (!isset($tree->branches->$key)) {
+ $tree->branches->$key = $this->newTree();
+ }
+ $tree = &$tree->branches->$key;
+ }
+ $tree->values[] = $value;
+ }
+
+ public function match(array $path): array
+ {
+ $star = self::WILDCARD;
+ $tree = &$this->tree;
+ foreach ($path as $key) {
+ if (isset($tree->branches->$key)) {
+ $tree = &$tree->branches->$key;
+ } elseif (isset($tree->branches->$star)) {
+ $tree = &$tree->branches->$star;
+ } else {
+ return [];
+ }
+ }
+ return $tree->values;
+ }
+
+ public static function fromJson(/* object */$tree): PathTree
+ {
+ return new PathTree($tree);
+ }
+
+ public function jsonSerialize()
+ {
+ return $this->tree;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/RecordService.php
+namespace Tqdev\PhpCrudApi\Record {
+
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Database\GenericDB;
+ use Tqdev\PhpCrudApi\Record\Document\ListDocument;
+
+ class RecordService
+ {
+ private $db;
+ private $reflection;
+ private $columns;
+ private $joiner;
+ private $filters;
+ private $ordering;
+ private $pagination;
+
+ public function __construct(GenericDB $db, ReflectionService $reflection)
+ {
+ $this->db = $db;
+ $this->reflection = $reflection;
+ $this->columns = new ColumnIncluder();
+ $this->joiner = new RelationJoiner($reflection, $this->columns);
+ $this->filters = new FilterInfo();
+ $this->ordering = new OrderingInfo();
+ $this->pagination = new PaginationInfo();
+ }
+
+ private function sanitizeRecord(string $tableName, /* object */ $record, string $id)
+ {
+ $keyset = array_keys((array) $record);
+ foreach ($keyset as $key) {
+ if (!$this->reflection->getTable($tableName)->hasColumn($key)) {
+ unset($record->$key);
+ }
+ }
+ if ($id != '') {
+ $pk = $this->reflection->getTable($tableName)->getPk();
+ foreach ($this->reflection->getTable($tableName)->getColumnNames() as $key) {
+ $field = $this->reflection->getTable($tableName)->getColumn($key);
+ if ($field->getName() == $pk->getName()) {
+ unset($record->$key);
+ }
+ }
+ }
+ }
+
+ public function hasTable(string $table): bool
+ {
+ return $this->reflection->hasTable($table);
+ }
+
+ public function getType(string $table): string
+ {
+ return $this->reflection->getType($table);
+ }
+
+ public function create(string $tableName, /* object */ $record, array $params) /*: ?int*/
+ {
+ $this->sanitizeRecord($tableName, $record, '');
+ $table = $this->reflection->getTable($tableName);
+ $columnValues = $this->columns->getValues($table, true, $record, $params);
+ return $this->db->createSingle($table, $columnValues);
+ }
+
+ public function read(string $tableName, string $id, array $params) /*: ?object*/
+ {
+ $table = $this->reflection->getTable($tableName);
+ $this->joiner->addMandatoryColumns($table, $params);
+ $columnNames = $this->columns->getNames($table, true, $params);
+ $record = $this->db->selectSingle($table, $columnNames, $id);
+ if ($record == null) {
+ return null;
+ }
+ $records = array($record);
+ $this->joiner->addJoins($table, $records, $params, $this->db);
+ return $records[0];
+ }
+
+ public function update(string $tableName, string $id, /* object */ $record, array $params) /*: ?int*/
+ {
+ $this->sanitizeRecord($tableName, $record, $id);
+ $table = $this->reflection->getTable($tableName);
+ $columnValues = $this->columns->getValues($table, true, $record, $params);
+ return $this->db->updateSingle($table, $columnValues, $id);
+ }
+
+ public function delete(string $tableName, string $id, array $params) /*: ?int*/
+ {
+ $table = $this->reflection->getTable($tableName);
+ return $this->db->deleteSingle($table, $id);
+ }
+
+ public function increment(string $tableName, string $id, /* object */ $record, array $params) /*: ?int*/
+ {
+ $this->sanitizeRecord($tableName, $record, $id);
+ $table = $this->reflection->getTable($tableName);
+ $columnValues = $this->columns->getValues($table, true, $record, $params);
+ return $this->db->incrementSingle($table, $columnValues, $id);
+ }
+
+ public function _list(string $tableName, array $params): ListDocument
+ {
+ $table = $this->reflection->getTable($tableName);
+ $this->joiner->addMandatoryColumns($table, $params);
+ $columnNames = $this->columns->getNames($table, true, $params);
+ $condition = $this->filters->getCombinedConditions($table, $params);
+ $columnOrdering = $this->ordering->getColumnOrdering($table, $params);
+ if (!$this->pagination->hasPage($params)) {
+ $offset = 0;
+ $limit = $this->pagination->getPageLimit($params);
+ $count = 0;
+ } else {
+ $offset = $this->pagination->getPageOffset($params);
+ $limit = $this->pagination->getPageLimit($params);
+ $count = $this->db->selectCount($table, $condition);
+ }
+ $records = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, $offset, $limit);
+ $this->joiner->addJoins($table, $records, $params, $this->db);
+ return new ListDocument($records, $count);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Record/RelationJoiner.php
+namespace Tqdev\PhpCrudApi\Record {
+
+ use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Database\GenericDB;
+ use Tqdev\PhpCrudApi\Middleware\Communication\VariableStore;
+ use Tqdev\PhpCrudApi\Record\Condition\ColumnCondition;
+ use Tqdev\PhpCrudApi\Record\Condition\OrCondition;
+
+ class RelationJoiner
+ {
+ private $reflection;
+ private $ordering;
+ private $columns;
+
+ public function __construct(ReflectionService $reflection, ColumnIncluder $columns)
+ {
+ $this->reflection = $reflection;
+ $this->ordering = new OrderingInfo();
+ $this->columns = $columns;
+ }
+
+ public function addMandatoryColumns(ReflectedTable $table, array &$params) /*: void*/
+ {
+ if (!isset($params['join']) || !isset($params['include'])) {
+ return;
+ }
+ $params['mandatory'] = array();
+ foreach ($params['join'] as $tableNames) {
+ $t1 = $table;
+ foreach (explode(',', $tableNames) as $tableName) {
+ if (!$this->reflection->hasTable($tableName)) {
+ continue;
+ }
+ $t2 = $this->reflection->getTable($tableName);
+ $fks1 = $t1->getFksTo($t2->getName());
+ $t3 = $this->hasAndBelongsToMany($t1, $t2);
+ if ($t3 != null || count($fks1) > 0) {
+ $params['mandatory'][] = $t2->getName() . '.' . $t2->getPk()->getName();
+ }
+ foreach ($fks1 as $fk) {
+ $params['mandatory'][] = $t1->getName() . '.' . $fk->getName();
+ }
+ $fks2 = $t2->getFksTo($t1->getName());
+ if ($t3 != null || count($fks2) > 0) {
+ $params['mandatory'][] = $t1->getName() . '.' . $t1->getPk()->getName();
+ }
+ foreach ($fks2 as $fk) {
+ $params['mandatory'][] = $t2->getName() . '.' . $fk->getName();
+ }
+ $t1 = $t2;
+ }
+ }
+ }
+
+ private function getJoinsAsPathTree(array $params): PathTree
+ {
+ $joins = new PathTree();
+ if (isset($params['join'])) {
+ foreach ($params['join'] as $tableNames) {
+ $path = array();
+ foreach (explode(',', $tableNames) as $tableName) {
+ if (!$this->reflection->hasTable($tableName)) {
+ continue;
+ }
+ $t = $this->reflection->getTable($tableName);
+ if ($t != null) {
+ $path[] = $t->getName();
+ }
+ }
+ $joins->put($path, true);
+ }
+ }
+ return $joins;
+ }
+
+ public function addJoins(ReflectedTable $table, array &$records, array $params, GenericDB $db) /*: void*/
+ {
+ $joins = $this->getJoinsAsPathTree($params);
+ $this->addJoinsForTables($table, $joins, $records, $params, $db);
+ }
+
+ private function hasAndBelongsToMany(ReflectedTable $t1, ReflectedTable $t2) /*: ?ReflectedTable*/
+ {
+ foreach ($this->reflection->getTableNames() as $tableName) {
+ $t3 = $this->reflection->getTable($tableName);
+ if (count($t3->getFksTo($t1->getName())) > 0 && count($t3->getFksTo($t2->getName())) > 0) {
+ return $t3;
+ }
+ }
+ return null;
+ }
+
+ private function addJoinsForTables(ReflectedTable $t1, PathTree $joins, array &$records, array $params, GenericDB $db)
+ {
+ foreach ($joins->getKeys() as $t2Name) {
+ $t2 = $this->reflection->getTable($t2Name);
+
+ $belongsTo = count($t1->getFksTo($t2->getName())) > 0;
+ $hasMany = count($t2->getFksTo($t1->getName())) > 0;
+ if (!$belongsTo && !$hasMany) {
+ $t3 = $this->hasAndBelongsToMany($t1, $t2);
+ } else {
+ $t3 = null;
+ }
+ $hasAndBelongsToMany = ($t3 != null);
+
+ $newRecords = array();
+ $fkValues = null;
+ $pkValues = null;
+ $habtmValues = null;
+
+ if ($belongsTo) {
+ $fkValues = $this->getFkEmptyValues($t1, $t2, $records);
+ $this->addFkRecords($t2, $fkValues, $params, $db, $newRecords);
+ }
+ if ($hasMany) {
+ $pkValues = $this->getPkEmptyValues($t1, $records);
+ $this->addPkRecords($t1, $t2, $pkValues, $params, $db, $newRecords);
+ }
+ if ($hasAndBelongsToMany) {
+ $habtmValues = $this->getHabtmEmptyValues($t1, $t2, $t3, $db, $records);
+ $this->addFkRecords($t2, $habtmValues->fkValues, $params, $db, $newRecords);
+ }
+
+ $this->addJoinsForTables($t2, $joins->get($t2Name), $newRecords, $params, $db);
+
+ if ($fkValues != null) {
+ $this->fillFkValues($t2, $newRecords, $fkValues);
+ $this->setFkValues($t1, $t2, $records, $fkValues);
+ }
+ if ($pkValues != null) {
+ $this->fillPkValues($t1, $t2, $newRecords, $pkValues);
+ $this->setPkValues($t1, $t2, $records, $pkValues);
+ }
+ if ($habtmValues != null) {
+ $this->fillFkValues($t2, $newRecords, $habtmValues->fkValues);
+ $this->setHabtmValues($t1, $t2, $records, $habtmValues);
+ }
+ }
+ }
+
+ private function getFkEmptyValues(ReflectedTable $t1, ReflectedTable $t2, array $records): array
+ {
+ $fkValues = array();
+ $fks = $t1->getFksTo($t2->getName());
+ foreach ($fks as $fk) {
+ $fkName = $fk->getName();
+ foreach ($records as $record) {
+ if (isset($record[$fkName])) {
+ $fkValue = $record[$fkName];
+ $fkValues[$fkValue] = null;
+ }
+ }
+ }
+ return $fkValues;
+ }
+
+ private function addFkRecords(ReflectedTable $t2, array $fkValues, array $params, GenericDB $db, array &$records) /*: void*/
+ {
+ $columnNames = $this->columns->getNames($t2, false, $params);
+ $fkIds = array_keys($fkValues);
+
+ foreach ($db->selectMultiple($t2, $columnNames, $fkIds) as $record) {
+ $records[] = $record;
+ }
+ }
+
+ private function fillFkValues(ReflectedTable $t2, array $fkRecords, array &$fkValues) /*: void*/
+ {
+ $pkName = $t2->getPk()->getName();
+ foreach ($fkRecords as $fkRecord) {
+ $pkValue = $fkRecord[$pkName];
+ $fkValues[$pkValue] = $fkRecord;
+ }
+ }
+
+ private function setFkValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, array $fkValues) /*: void*/
+ {
+ $fks = $t1->getFksTo($t2->getName());
+ foreach ($fks as $fk) {
+ $fkName = $fk->getName();
+ foreach ($records as $i => $record) {
+ if (isset($record[$fkName])) {
+ $key = $record[$fkName];
+ $records[$i][$fkName] = $fkValues[$key];
+ }
+ }
+ }
+ }
+
+ private function getPkEmptyValues(ReflectedTable $t1, array $records): array
+ {
+ $pkValues = array();
+ $pkName = $t1->getPk()->getName();
+ foreach ($records as $record) {
+ $key = $record[$pkName];
+ $pkValues[$key] = array();
+ }
+ return $pkValues;
+ }
+
+ private function addPkRecords(ReflectedTable $t1, ReflectedTable $t2, array $pkValues, array $params, GenericDB $db, array &$records) /*: void*/
+ {
+ $fks = $t2->getFksTo($t1->getName());
+ $columnNames = $this->columns->getNames($t2, false, $params);
+ $pkValueKeys = implode(',', array_keys($pkValues));
+ $conditions = array();
+ foreach ($fks as $fk) {
+ $conditions[] = new ColumnCondition($fk, 'in', $pkValueKeys);
+ }
+ $condition = OrCondition::fromArray($conditions);
+ $columnOrdering = array();
+ $limit = VariableStore::get("joinLimits.maxRecords") ?: -1;
+ if ($limit != -1) {
+ $columnOrdering = $this->ordering->getDefaultColumnOrdering($t2);
+ }
+ foreach ($db->selectAll($t2, $columnNames, $condition, $columnOrdering, 0, $limit) as $record) {
+ $records[] = $record;
+ }
+ }
+
+ private function fillPkValues(ReflectedTable $t1, ReflectedTable $t2, array $pkRecords, array &$pkValues) /*: void*/
+ {
+ $fks = $t2->getFksTo($t1->getName());
+ foreach ($fks as $fk) {
+ $fkName = $fk->getName();
+ foreach ($pkRecords as $pkRecord) {
+ $key = $pkRecord[$fkName];
+ if (isset($pkValues[$key])) {
+ $pkValues[$key][] = $pkRecord;
+ }
+ }
+ }
+ }
+
+ private function setPkValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, array $pkValues) /*: void*/
+ {
+ $pkName = $t1->getPk()->getName();
+ $t2Name = $t2->getName();
+
+ foreach ($records as $i => $record) {
+ $key = $record[$pkName];
+ $records[$i][$t2Name] = $pkValues[$key];
+ }
+ }
+
+ private function getHabtmEmptyValues(ReflectedTable $t1, ReflectedTable $t2, ReflectedTable $t3, GenericDB $db, array $records): HabtmValues
+ {
+ $pkValues = $this->getPkEmptyValues($t1, $records);
+ $fkValues = array();
+
+ $fk1 = $t3->getFksTo($t1->getName())[0];
+ $fk2 = $t3->getFksTo($t2->getName())[0];
+
+ $fk1Name = $fk1->getName();
+ $fk2Name = $fk2->getName();
+
+ $columnNames = array($fk1Name, $fk2Name);
+
+ $pkIds = implode(',', array_keys($pkValues));
+ $condition = new ColumnCondition($t3->getColumn($fk1Name), 'in', $pkIds);
+ $columnOrdering = array();
+
+ $limit = VariableStore::get("joinLimits.maxRecords") ?: -1;
+ if ($limit != -1) {
+ $columnOrdering = $this->ordering->getDefaultColumnOrdering($t3);
+ }
+ $records = $db->selectAll($t3, $columnNames, $condition, $columnOrdering, 0, $limit);
+ foreach ($records as $record) {
+ $val1 = $record[$fk1Name];
+ $val2 = $record[$fk2Name];
+ $pkValues[$val1][] = $val2;
+ $fkValues[$val2] = null;
+ }
+
+ return new HabtmValues($pkValues, $fkValues);
+ }
+
+ private function setHabtmValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, HabtmValues $habtmValues) /*: void*/
+ {
+ $pkName = $t1->getPk()->getName();
+ $t2Name = $t2->getName();
+ foreach ($records as $i => $record) {
+ $key = $record[$pkName];
+ $val = array();
+ $fks = $habtmValues->pkValues[$key];
+ foreach ($fks as $fk) {
+ $val[] = $habtmValues->fkValues[$fk];
+ }
+ $records[$i][$t2Name] = $val;
+ }
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Api.php
+namespace Tqdev\PhpCrudApi {
+
+ use Psr\Http\Message\ResponseInterface;
+ use Psr\Http\Message\ServerRequestInterface;
+ use Psr\Http\Server\RequestHandlerInterface;
+ use Tqdev\PhpCrudApi\Cache\CacheFactory;
+ use Tqdev\PhpCrudApi\Column\DefinitionService;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+ use Tqdev\PhpCrudApi\Controller\CacheController;
+ use Tqdev\PhpCrudApi\Controller\ColumnController;
+ use Tqdev\PhpCrudApi\Controller\GeoJsonController;
+ use Tqdev\PhpCrudApi\Controller\JsonResponder;
+ use Tqdev\PhpCrudApi\Controller\OpenApiController;
+ use Tqdev\PhpCrudApi\Controller\RecordController;
+ use Tqdev\PhpCrudApi\Database\GenericDB;
+ use Tqdev\PhpCrudApi\GeoJson\GeoJsonService;
+ use Tqdev\PhpCrudApi\Middleware\AuthorizationMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\BasicAuthMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\CorsMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\CustomizationMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\DbAuthMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\FirewallMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\IpAddressMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\JoinLimitsMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\JwtAuthMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\MultiTenancyMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\PageLimitsMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\ReconnectMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\Router\SimpleRouter;
+ use Tqdev\PhpCrudApi\Middleware\SanitationMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\SslRedirectMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\ValidationMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\XmlMiddleware;
+ use Tqdev\PhpCrudApi\Middleware\XsrfMiddleware;
+ use Tqdev\PhpCrudApi\OpenApi\OpenApiService;
+ use Tqdev\PhpCrudApi\Record\ErrorCode;
+ use Tqdev\PhpCrudApi\Record\RecordService;
+ use Tqdev\PhpCrudApi\ResponseUtils;
+
+ class Api implements RequestHandlerInterface
+ {
+ private $router;
+ private $responder;
+ private $debug;
+
+ public function __construct(Config $config)
+ {
+ $db = new GenericDB(
+ $config->getDriver(),
+ $config->getAddress(),
+ $config->getPort(),
+ $config->getDatabase(),
+ $config->getTables(),
+ $config->getUsername(),
+ $config->getPassword()
+ );
+ $prefix = sprintf('phpcrudapi-%s-', substr(md5(__FILE__), 0, 8));
+ $cache = CacheFactory::create($config->getCacheType(), $prefix, $config->getCachePath());
+ $reflection = new ReflectionService($db, $cache, $config->getCacheTime());
+ $responder = new JsonResponder();
+ $router = new SimpleRouter($config->getBasePath(), $responder, $cache, $config->getCacheTime(), $config->getDebug());
+ foreach ($config->getMiddlewares() as $middleware => $properties) {
+ switch ($middleware) {
+ case 'sslRedirect':
+ new SslRedirectMiddleware($router, $responder, $properties);
+ break;
+ case 'cors':
+ new CorsMiddleware($router, $responder, $properties, $config->getDebug());
+ break;
+ case 'firewall':
+ new FirewallMiddleware($router, $responder, $properties);
+ break;
+ case 'basicAuth':
+ new BasicAuthMiddleware($router, $responder, $properties);
+ break;
+ case 'jwtAuth':
+ new JwtAuthMiddleware($router, $responder, $properties);
+ break;
+ case 'dbAuth':
+ new DbAuthMiddleware($router, $responder, $properties, $reflection, $db);
+ break;
+ case 'reconnect':
+ new ReconnectMiddleware($router, $responder, $properties, $reflection, $db);
+ break;
+ case 'validation':
+ new ValidationMiddleware($router, $responder, $properties, $reflection);
+ break;
+ case 'ipAddress':
+ new IpAddressMiddleware($router, $responder, $properties, $reflection);
+ break;
+ case 'sanitation':
+ new SanitationMiddleware($router, $responder, $properties, $reflection);
+ break;
+ case 'multiTenancy':
+ new MultiTenancyMiddleware($router, $responder, $properties, $reflection);
+ break;
+ case 'authorization':
+ new AuthorizationMiddleware($router, $responder, $properties, $reflection);
+ break;
+ case 'xsrf':
+ new XsrfMiddleware($router, $responder, $properties);
+ break;
+ case 'pageLimits':
+ new PageLimitsMiddleware($router, $responder, $properties, $reflection);
+ break;
+ case 'joinLimits':
+ new JoinLimitsMiddleware($router, $responder, $properties, $reflection);
+ break;
+ case 'customization':
+ new CustomizationMiddleware($router, $responder, $properties, $reflection);
+ break;
+ case 'xml':
+ new XmlMiddleware($router, $responder, $properties, $reflection);
+ break;
+ }
+ }
+ foreach ($config->getControllers() as $controller) {
+ switch ($controller) {
+ case 'records':
+ $records = new RecordService($db, $reflection);
+ new RecordController($router, $responder, $records);
+ break;
+ case 'columns':
+ $definition = new DefinitionService($db, $reflection);
+ new ColumnController($router, $responder, $reflection, $definition);
+ break;
+ case 'cache':
+ new CacheController($router, $responder, $cache);
+ break;
+ case 'openapi':
+ $openApi = new OpenApiService($reflection, $config->getOpenApiBase(), $config->getControllers(), $config->getCustomOpenApiBuilders());
+ new OpenApiController($router, $responder, $openApi);
+ break;
+ case 'geojson':
+ $records = new RecordService($db, $reflection);
+ $geoJson = new GeoJsonService($reflection, $records);
+ new GeoJsonController($router, $responder, $geoJson);
+ break;
+ }
+ }
+ foreach ($config->getCustomControllers() as $className) {
+ if (class_exists($className)) {
+ $records = new RecordService($db, $reflection);
+ new $className($router, $responder, $records);
+ }
+ }
+ $this->router = $router;
+ $this->responder = $responder;
+ $this->debug = $config->getDebug();
+ }
+
+ private function parseBody(string $body) /*: ?object*/
+ {
+ $first = substr($body, 0, 1);
+ if ($first == '[' || $first == '{') {
+ $object = json_decode($body);
+ $causeCode = json_last_error();
+ if ($causeCode !== JSON_ERROR_NONE) {
+ $object = null;
+ }
+ } else {
+ parse_str($body, $input);
+ foreach ($input as $key => $value) {
+ if (substr($key, -9) == '__is_null') {
+ $input[substr($key, 0, -9)] = null;
+ unset($input[$key]);
+ }
+ }
+ $object = (object) $input;
+ }
+ return $object;
+ }
+
+ private function addParsedBody(ServerRequestInterface $request): ServerRequestInterface
+ {
+ $parsedBody = $request->getParsedBody();
+ if ($parsedBody) {
+ $request = $this->applyParsedBodyHack($request);
+ } else {
+ $body = $request->getBody();
+ if ($body->isReadable()) {
+ if ($body->isSeekable()) {
+ $body->rewind();
+ }
+ $contents = $body->getContents();
+ if ($body->isSeekable()) {
+ $body->rewind();
+ }
+ if ($contents) {
+ $parsedBody = $this->parseBody($contents);
+ $request = $request->withParsedBody($parsedBody);
+ }
+ }
+ }
+ return $request;
+ }
+
+ private function applyParsedBodyHack(ServerRequestInterface $request): ServerRequestInterface
+ {
+ $parsedBody = $request->getParsedBody();
+ if (is_array($parsedBody)) { // is it really?
+ $contents = json_encode($parsedBody);
+ $parsedBody = $this->parseBody($contents);
+ $request = $request->withParsedBody($parsedBody);
+ }
+ return $request;
+ }
+
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ return $this->router->route($this->addParsedBody($request));
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/Config.php
+namespace Tqdev\PhpCrudApi {
+
+ class Config
+ {
+ private $values = [
+ 'driver' => null,
+ 'address' => 'localhost',
+ 'port' => null,
+ 'username' => null,
+ 'password' => null,
+ 'database' => null,
+ 'tables' => '',
+ 'middlewares' => 'cors,errors',
+ 'controllers' => 'records,geojson,openapi',
+ 'customControllers' => '',
+ 'customOpenApiBuilders' => '',
+ 'cacheType' => 'TempFile',
+ 'cachePath' => '',
+ 'cacheTime' => 10,
+ 'debug' => false,
+ 'basePath' => '',
+ 'openApiBase' => '{"info":{"title":"PHP-CRUD-API","version":"1.0.0"}}',
+ ];
+
+ private function getDefaultDriver(array $values): string
+ {
+ if (isset($values['driver'])) {
+ return $values['driver'];
+ }
+ return 'mysql';
+ }
+
+ private function getDefaultPort(string $driver): int
+ {
+ switch ($driver) {
+ case 'mysql':
+ return 3306;
+ case 'pgsql':
+ return 5432;
+ case 'sqlsrv':
+ return 1433;
+ case 'sqlite':
+ return 0;
+ }
+ }
+
+ private function getDefaultAddress(string $driver): string
+ {
+ switch ($driver) {
+ case 'mysql':
+ return 'localhost';
+ case 'pgsql':
+ return 'localhost';
+ case 'sqlsrv':
+ return 'localhost';
+ case 'sqlite':
+ return 'data.db';
+ }
+ }
+
+ private function getDriverDefaults(string $driver): array
+ {
+ return [
+ 'driver' => $driver,
+ 'address' => $this->getDefaultAddress($driver),
+ 'port' => $this->getDefaultPort($driver),
+ ];
+ }
+
+ private function applyEnvironmentVariables(array $values): array
+ {
+ $newValues = array();
+ foreach ($values as $key => $value) {
+ $environmentKey = 'PHP_CRUD_API_' . strtoupper(preg_replace('/(?getDefaultDriver($values);
+ $defaults = $this->getDriverDefaults($driver);
+ $newValues = array_merge($this->values, $defaults, $values);
+ $newValues = $this->parseMiddlewares($newValues);
+ $diff = array_diff_key($newValues, $this->values);
+ if (!empty($diff)) {
+ $key = array_keys($diff)[0];
+ throw new \Exception("Config has invalid value '$key'");
+ }
+ $newValues = $this->applyEnvironmentVariables($newValues);
+ $this->values = $newValues;
+ }
+
+ private function parseMiddlewares(array $values): array
+ {
+ $newValues = array();
+ $properties = array();
+ $middlewares = array_map('trim', explode(',', $values['middlewares']));
+ foreach ($middlewares as $middleware) {
+ $properties[$middleware] = [];
+ }
+ foreach ($values as $key => $value) {
+ if (strpos($key, '.') === false) {
+ $newValues[$key] = $value;
+ } else {
+ list($middleware, $key2) = explode('.', $key, 2);
+ if (isset($properties[$middleware])) {
+ $properties[$middleware][$key2] = $value;
+ } else {
+ throw new \Exception("Config has invalid value '$key'");
+ }
+ }
+ }
+ $newValues['middlewares'] = array_reverse($properties, true);
+ return $newValues;
+ }
+
+ public function getDriver(): string
+ {
+ return $this->values['driver'];
+ }
+
+ public function getAddress(): string
+ {
+ return $this->values['address'];
+ }
+
+ public function getPort(): int
+ {
+ return $this->values['port'];
+ }
+
+ public function getUsername(): string
+ {
+ return $this->values['username'];
+ }
+
+ public function getPassword(): string
+ {
+ return $this->values['password'];
+ }
+
+ public function getDatabase(): string
+ {
+ return $this->values['database'];
+ }
+
+ public function getTables(): array
+ {
+ return array_filter(array_map('trim', explode(',', $this->values['tables'])));
+ }
+
+ public function getMiddlewares(): array
+ {
+ return $this->values['middlewares'];
+ }
+
+ public function getControllers(): array
+ {
+ return array_filter(array_map('trim', explode(',', $this->values['controllers'])));
+ }
+
+ public function getCustomControllers(): array
+ {
+ return array_filter(array_map('trim', explode(',', $this->values['customControllers'])));
+ }
+
+ public function getCustomOpenApiBuilders(): array
+ {
+ return array_filter(array_map('trim', explode(',', $this->values['customOpenApiBuilders'])));
+ }
+
+ public function getCacheType(): string
+ {
+ return $this->values['cacheType'];
+ }
+
+ public function getCachePath(): string
+ {
+ return $this->values['cachePath'];
+ }
+
+ public function getCacheTime(): int
+ {
+ return $this->values['cacheTime'];
+ }
+
+ public function getDebug(): bool
+ {
+ return $this->values['debug'];
+ }
+
+ public function getBasePath(): string
+ {
+ return $this->values['basePath'];
+ }
+
+ public function getOpenApiBase(): array
+ {
+ return json_decode($this->values['openApiBase'], true);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/RequestFactory.php
+namespace Tqdev\PhpCrudApi {
+
+ use Nyholm\Psr7\Factory\Psr17Factory;
+ use Nyholm\Psr7Server\ServerRequestCreator;
+ use Psr\Http\Message\ServerRequestInterface;
+
+ class RequestFactory
+ {
+ public static function fromGlobals(): ServerRequestInterface
+ {
+ $psr17Factory = new Psr17Factory();
+ $creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
+ $serverRequest = $creator->fromGlobals();
+ $stream = $psr17Factory->createStreamFromFile('php://input');
+ $serverRequest = $serverRequest->withBody($stream);
+ return $serverRequest;
+ }
+
+ public static function fromString(string $request): ServerRequestInterface
+ {
+ $parts = explode("\n\n", trim($request), 2);
+ $lines = explode("\n", $parts[0]);
+ $first = explode(' ', trim(array_shift($lines)), 2);
+ $method = $first[0];
+ $body = isset($parts[1]) ? $parts[1] : '';
+ $url = isset($first[1]) ? $first[1] : '';
+
+ $psr17Factory = new Psr17Factory();
+ $serverRequest = $psr17Factory->createServerRequest($method, $url);
+ foreach ($lines as $line) {
+ list($key, $value) = explode(':', $line, 2);
+ $serverRequest = $serverRequest->withAddedHeader($key, $value);
+ }
+ if ($body) {
+ $stream = $psr17Factory->createStream($body);
+ $stream->rewind();
+ $serverRequest = $serverRequest->withBody($stream);
+ }
+ return $serverRequest;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/RequestUtils.php
+namespace Tqdev\PhpCrudApi {
+
+ use Psr\Http\Message\ServerRequestInterface;
+ use Tqdev\PhpCrudApi\Column\ReflectionService;
+
+ class RequestUtils
+ {
+ public static function setParams(ServerRequestInterface $request, array $params): ServerRequestInterface
+ {
+ $query = preg_replace('|%5B[0-9]+%5D=|', '=', http_build_query($params));
+ return $request->withUri($request->getUri()->withQuery($query));
+ }
+
+ public static function getHeader(ServerRequestInterface $request, string $header): string
+ {
+ $headers = $request->getHeader($header);
+ return isset($headers[0]) ? $headers[0] : '';
+ }
+
+ public static function getParams(ServerRequestInterface $request): array
+ {
+ $params = array();
+ $query = $request->getUri()->getQuery();
+ //$query = str_replace('][]=', ']=', str_replace('=', '[]=', $query));
+ $query = str_replace('%5D%5B%5D=', '%5D=', str_replace('=', '%5B%5D=', $query));
+ parse_str($query, $params);
+ return $params;
+ }
+
+ public static function getPathSegment(ServerRequestInterface $request, int $part): string
+ {
+ $path = $request->getUri()->getPath();
+ $pathSegments = explode('/', rtrim($path, '/'));
+ if ($part < 0 || $part >= count($pathSegments)) {
+ return '';
+ }
+ return urldecode($pathSegments[$part]);
+ }
+
+ public static function getOperation(ServerRequestInterface $request): string
+ {
+ $method = $request->getMethod();
+ $path = RequestUtils::getPathSegment($request, 1);
+ $hasPk = RequestUtils::getPathSegment($request, 3) != '';
+ switch ($path) {
+ case 'openapi':
+ return 'document';
+ case 'columns':
+ return $method == 'get' ? 'reflect' : 'remodel';
+ case 'geojson':
+ case 'records':
+ switch ($method) {
+ case 'POST':
+ return 'create';
+ case 'GET':
+ return $hasPk ? 'read' : 'list';
+ case 'PUT':
+ return 'update';
+ case 'DELETE':
+ return 'delete';
+ case 'PATCH':
+ return 'increment';
+ }
+ }
+ return 'unknown';
+ }
+
+ private static function getJoinTables(string $tableName, array $parameters): array
+ {
+ $uniqueTableNames = array();
+ $uniqueTableNames[$tableName] = true;
+ if (isset($parameters['join'])) {
+ foreach ($parameters['join'] as $parameter) {
+ $tableNames = explode(',', trim($parameter));
+ foreach ($tableNames as $tableName) {
+ $uniqueTableNames[$tableName] = true;
+ }
+ }
+ }
+ return array_keys($uniqueTableNames);
+ }
+
+ public static function getTableNames(ServerRequestInterface $request, ReflectionService $reflection): array
+ {
+ $path = RequestUtils::getPathSegment($request, 1);
+ $tableName = RequestUtils::getPathSegment($request, 2);
+ $allTableNames = $reflection->getTableNames();
+ switch ($path) {
+ case 'openapi':
+ return $allTableNames;
+ case 'columns':
+ return $tableName ? [$tableName] : $allTableNames;
+ case 'records':
+ return self::getJoinTables($tableName, RequestUtils::getParams($request));
+ }
+ return $allTableNames;
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/ResponseFactory.php
+namespace Tqdev\PhpCrudApi {
+
+ use Nyholm\Psr7\Factory\Psr17Factory;
+ use Psr\Http\Message\ResponseInterface;
+
+ class ResponseFactory
+ {
+ const OK = 200;
+ const MOVED_PERMANENTLY = 301;
+ const FOUND = 302;
+ const UNAUTHORIZED = 401;
+ const FORBIDDEN = 403;
+ const NOT_FOUND = 404;
+ const METHOD_NOT_ALLOWED = 405;
+ const CONFLICT = 409;
+ const UNPROCESSABLE_ENTITY = 422;
+ const INTERNAL_SERVER_ERROR = 500;
+
+ public static function fromXml(int $status, string $xml): ResponseInterface
+ {
+ return self::from($status, 'text/xml', $xml);
+ }
+
+ public static function fromCsv(int $status, string $csv): ResponseInterface
+ {
+ return self::from($status, 'text/csv', $csv);
+ }
+
+ public static function fromHtml(int $status, string $html): ResponseInterface
+ {
+ return self::from($status, 'text/html', $html);
+ }
+
+ public static function fromObject(int $status, $body): ResponseInterface
+ {
+ $content = json_encode($body, JSON_UNESCAPED_UNICODE);
+ return self::from($status, 'application/json', $content);
+ }
+
+ public static function from(int $status, string $contentType, string $content): ResponseInterface
+ {
+ $psr17Factory = new Psr17Factory();
+ $response = $psr17Factory->createResponse($status);
+ $stream = $psr17Factory->createStream($content);
+ $stream->rewind();
+ $response = $response->withBody($stream);
+ $response = $response->withHeader('Content-Type', $contentType . '; charset=utf-8');
+ $response = $response->withHeader('Content-Length', strlen($content));
+ return $response;
+ }
+
+ public static function fromStatus(int $status): ResponseInterface
+ {
+ $psr17Factory = new Psr17Factory();
+ return $psr17Factory->createResponse($status);
+ }
+ }
+}
+
+// file: src/Tqdev/PhpCrudApi/ResponseUtils.php
+namespace Tqdev\PhpCrudApi {
+
+ use Psr\Http\Message\ResponseInterface;
+
+ class ResponseUtils
+ {
+ public static function output(ResponseInterface $response)
+ {
+ $status = $response->getStatusCode();
+ $headers = $response->getHeaders();
+ $body = $response->getBody()->getContents();
+
+ http_response_code($status);
+ foreach ($headers as $key => $values) {
+ foreach ($values as $value) {
+ header("$key: $value");
+ }
+ }
+ echo $body;
+ }
+
+ public static function addExceptionHeaders(ResponseInterface $response, \Throwable $e): ResponseInterface
+ {
+ $response = $response->withHeader('X-Exception-Name', get_class($e));
+ $response = $response->withHeader('X-Exception-Message', preg_replace('|\n|', ' ', trim($e->getMessage())));
+ $response = $response->withHeader('X-Exception-File', $e->getFile() . ':' . $e->getLine());
+ return $response;
+ }
+
+ public static function toString(ResponseInterface $response): string
+ {
+ $status = $response->getStatusCode();
+ $headers = $response->getHeaders();
+ $body = $response->getBody()->getContents();
+
+ $str = "$status\n";
+ foreach ($headers as $key => $values) {
+ foreach ($values as $value) {
+ $str .= "$key: $value\n";
+ }
+ }
+ if ($body !== '') {
+ $str .= "\n";
+ $str .= "$body\n";
+ }
+ return $str;
+ }
+ }
+}
+
+// file: src/index.php
+namespace Tqdev\PhpCrudApi {
+
+ use Tqdev\PhpCrudApi\Api;
+ use Tqdev\PhpCrudApi\Config;
+ use Tqdev\PhpCrudApi\RequestFactory;
+ use Tqdev\PhpCrudApi\ResponseUtils;
+
+ $config = new Config([
+ // 'driver' => 'mysql',
+ // 'address' => 'localhost',
+ // 'port' => '3306',
+ 'username' => 'php-crud-api',
+ 'password' => 'php-crud-api',
+ 'database' => 'php-crud-api',
+ // 'debug' => false
+ ]);
+ $request = RequestFactory::fromGlobals();
+ $api = new Api($config);
+ $response = $api->handle($request);
+ ResponseUtils::output($response);
+}
diff --git a/seatmap-webapi/build.php b/seatmap-webapi/build.php
new file mode 100644
index 0000000..fe7335a
--- /dev/null
+++ b/seatmap-webapi/build.php
@@ -0,0 +1,113 @@
+ $entry) {
+ if (isset($ignore[$dir . '/' . $entry])) {
+ unset($entries[$i]);
+ }
+ }
+}
+
+function runDir(string $base, string $dir, array &$lines, array $ignore): int
+{
+ $count = 0;
+ $entries = scandir($dir);
+ removeIgnored($dir, $entries, $ignore);
+ sort($entries);
+ foreach ($entries as $entry) {
+ if ($entry === '.' || $entry === '..') {
+ continue;
+ }
+ $filename = "$base/$dir/$entry";
+ if (is_dir($filename)) {
+ $count += runDir($base, "$dir/$entry", $lines, $ignore);
+ }
+ }
+ foreach ($entries as $entry) {
+ $filename = "$base/$dir/$entry";
+ if (is_file($filename)) {
+ if (substr($entry, -4) != '.php') {
+ continue;
+ }
+ $data = file_get_contents($filename);
+ $data = preg_replace('/\s*<\?php\s+/s', '', $data, 1);
+ $data = preg_replace('/^.*?(vendor\/autoload|declare\s*\(\s*strict_types\s*=\s*1).*?$/m', '', $data);
+ array_push($lines, "// file: $dir/$entry");
+ if (!preg_match('/^\s*(namespace[^;]*);/', $data)) {
+ $data = "namespace;\n" . $data;
+ }
+ foreach (explode("\n", trim($data)) as $line) {
+ if ($line) {
+ $line = ' ' . $line;
+ }
+ $line = preg_replace('/^\s*(namespace[^;]*);/', '\1 {', $line);
+ array_push($lines, $line);
+ }
+ array_push($lines, '}');
+ array_push($lines, '');
+ $count++;
+ }
+ }
+ return $count;
+}
+
+function addHeader(array &$lines)
+{
+ $head = <<=7.0.0",
+ "ext-zlib": "*",
+ "ext-json": "*",
+ "ext-pdo": "*",
+ "psr/http-message": "*",
+ "psr/http-factory": "*",
+ "psr/http-server-handler": "*",
+ "psr/http-server-middleware": "*",
+ "nyholm/psr7": "*",
+ "nyholm/psr7-server": "*"
+ },
+ "suggest": {
+ "ext-memcache": "*",
+ "ext-memcached": "*",
+ "ext-redis": "*"
+ },
+ "autoload": {
+ "psr-4": { "Tqdev\\PhpCrudApi\\": "src/Tqdev/PhpCrudApi" }
+ }
+}
diff --git a/seatmap-webapi/composer.lock b/seatmap-webapi/composer.lock
new file mode 100644
index 0000000..1660770
--- /dev/null
+++ b/seatmap-webapi/composer.lock
@@ -0,0 +1,396 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "3667d105fa59f4e36775fe664aef0f2f",
+ "packages": [
+ {
+ "name": "nyholm/psr7",
+ "version": "1.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Nyholm/psr7.git",
+ "reference": "55ff6b76573f5b242554c9775792bd59fb52e11c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Nyholm/psr7/zipball/55ff6b76573f5b242554c9775792bd59fb52e11c",
+ "reference": "55ff6b76573f5b242554c9775792bd59fb52e11c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1",
+ "php-http/message-factory": "^1.0",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.0"
+ },
+ "provide": {
+ "psr/http-factory-implementation": "1.0",
+ "psr/http-message-implementation": "1.0"
+ },
+ "require-dev": {
+ "http-interop/http-factory-tests": "dev-master",
+ "php-http/psr7-integration-tests": "dev-master",
+ "phpunit/phpunit": "^7.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Nyholm\\Psr7\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com"
+ },
+ {
+ "name": "Martijn van der Ven",
+ "email": "martijn@vanderven.se"
+ }
+ ],
+ "description": "A fast PHP7 implementation of PSR-7",
+ "homepage": "http://tnyholm.se",
+ "keywords": [
+ "psr-17",
+ "psr-7"
+ ],
+ "time": "2019-09-05T13:24:16+00:00"
+ },
+ {
+ "name": "nyholm/psr7-server",
+ "version": "0.4.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Nyholm/psr7-server.git",
+ "reference": "e6a526e9170e6e33a13efc2b61703ca476b7ea68"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Nyholm/psr7-server/zipball/e6a526e9170e6e33a13efc2b61703ca476b7ea68",
+ "reference": "e6a526e9170e6e33a13efc2b61703ca476b7ea68",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.0"
+ },
+ "require-dev": {
+ "nyholm/nsa": "^1.1",
+ "nyholm/psr7": "^1.0",
+ "phpunit/phpunit": "^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Nyholm\\Psr7Server\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com"
+ },
+ {
+ "name": "Martijn van der Ven",
+ "email": "martijn@vanderven.se"
+ }
+ ],
+ "description": "Helper classes to handle PSR-7 server requests",
+ "homepage": "http://tnyholm.se",
+ "keywords": [
+ "psr-17",
+ "psr-7"
+ ],
+ "time": "2019-11-05T20:36:33+00:00"
+ },
+ {
+ "name": "php-http/message-factory",
+ "version": "v1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-http/message-factory.git",
+ "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-http/message-factory/zipball/a478cb11f66a6ac48d8954216cfed9aa06a501a1",
+ "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4",
+ "psr/http-message": "^1.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com"
+ }
+ ],
+ "description": "Factory interfaces for PSR-7 HTTP Message",
+ "homepage": "http://php-http.org",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "stream",
+ "uri"
+ ],
+ "time": "2015-12-19T14:08:53+00:00"
+ },
+ {
+ "name": "psr/http-factory",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-factory.git",
+ "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be",
+ "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0.0",
+ "psr/http-message": "^1.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interfaces for PSR-7 HTTP message factories",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "psr",
+ "psr-17",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "time": "2019-04-30T12:38:16+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
+ "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "time": "2016-08-06T14:39:51+00:00"
+ },
+ {
+ "name": "psr/http-server-handler",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-server-handler.git",
+ "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/aff2f80e33b7f026ec96bb42f63242dc50ffcae7",
+ "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0",
+ "psr/http-message": "^1.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Server\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP server-side request handler",
+ "keywords": [
+ "handler",
+ "http",
+ "http-interop",
+ "psr",
+ "psr-15",
+ "psr-7",
+ "request",
+ "response",
+ "server"
+ ],
+ "time": "2018-10-30T16:46:14+00:00"
+ },
+ {
+ "name": "psr/http-server-middleware",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-server-middleware.git",
+ "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/2296f45510945530b9dceb8bcedb5cb84d40c5f5",
+ "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0",
+ "psr/http-message": "^1.0",
+ "psr/http-server-handler": "^1.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Server\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP server-side middleware",
+ "keywords": [
+ "http",
+ "http-interop",
+ "middleware",
+ "psr",
+ "psr-15",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "time": "2018-10-30T17:12:04+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {
+ "php": ">=7.0.0",
+ "ext-zlib": "*",
+ "ext-json": "*",
+ "ext-pdo": "*"
+ },
+ "platform-dev": [],
+ "plugin-api-version": "1.1.0"
+}
diff --git a/seatmap-webapi/docker-compose.yml b/seatmap-webapi/docker-compose.yml
new file mode 100644
index 0000000..405da65
--- /dev/null
+++ b/seatmap-webapi/docker-compose.yml
@@ -0,0 +1,31 @@
+version: '3'
+services:
+ database:
+ image: mysql:8.0
+ container_name: database
+ restart: always
+ environment:
+ - MYSQL_ROOT_PASSWORD=php-crud-api
+ - MYSQL_DATABASE=php-crud-api
+ - MYSQL_USER=php-crud-api
+ - MYSQL_PASSWORD=php-crud-api
+ #ports:
+ #- "33066:3306"
+ volumes:
+ - ./tests/fixtures/blog_mysql.sql:/docker-entrypoint-initdb.d/blog_mysql.sql
+ webserver:
+ container_name: webserver
+ build:
+ context: ./
+ environment:
+ #- PHP_CRUD_API_DRIVER=mysql
+ - PHP_CRUD_API_ADDRESS=database
+ #- PHP_CRUD_API_PORT=3306
+ #- PHP_CRUD_API_DATABASE=php-crud-api
+ #- PHP_CRUD_API_USERNAME=php-crud-api
+ #- PHP_CRUD_API_PASSWORD=php-crud-api
+ #- PHP_CRUD_API_DEBUG=1
+ ports:
+ - "8080:80"
+ depends_on:
+ - database
diff --git a/seatmap-webapi/docker/build_all.sh b/seatmap-webapi/docker/build_all.sh
new file mode 100755
index 0000000..24ef64e
--- /dev/null
+++ b/seatmap-webapi/docker/build_all.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+FILES=*
+
+for f in $FILES
+do
+if [[ -d "$f" ]]
+then
+ docker build "$f" -t "php-crud-api:$f"
+fi
+done
diff --git a/seatmap-webapi/docker/centos8/Dockerfile b/seatmap-webapi/docker/centos8/Dockerfile
new file mode 100644
index 0000000..625fdfc
--- /dev/null
+++ b/seatmap-webapi/docker/centos8/Dockerfile
@@ -0,0 +1,36 @@
+FROM centos:8
+
+# add this to avoid locale warnings
+RUN dnf -y install glibc-locale-source glibc-langpack-en
+RUN localedef -i en_US -f UTF-8 en_US.UTF-8
+
+# add utils for repos
+RUN dnf -y install wget dnf-utils
+
+# enable remi repo for php
+RUN dnf -y install http://rpms.remirepo.net/enterprise/remi-release-8.rpm
+# enable mariadb repo
+RUN wget https://downloads.mariadb.com/MariaDB/mariadb_repo_setup && bash mariadb_repo_setup
+# enable the postgresql repo
+RUN dnf -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm
+# enable epel repo
+RUN dnf -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm
+# enable powertools repos
+RUN dnf -y install 'dnf-command(config-manager)' && dnf -y config-manager --set-enabled PowerTools
+
+# set php to remi 7.4
+RUN dnf -y module reset php && dnf -y module enable php:remi-7.4
+# disable mariadb and postgresql default (appstream) repo
+RUN dnf -y module disable mariadb
+RUN dnf -y module disable postgresql
+
+RUN dnf -y install \
+php-cli php-xml php-json php-mbstring \
+MariaDB-server MariaDB-client php-mysqlnd \
+postgresql12 postgresql12-server php-pgsql postgis30_12 \
+sqlite php-sqlite3 \
+git wget
+
+# install run script
+ADD run.sh /usr/sbin/docker-run
+CMD docker-run
\ No newline at end of file
diff --git a/seatmap-webapi/docker/centos8/run.sh b/seatmap-webapi/docker/centos8/run.sh
new file mode 100755
index 0000000..189abe8
--- /dev/null
+++ b/seatmap-webapi/docker/centos8/run.sh
@@ -0,0 +1,60 @@
+#!/bin/bash
+echo "================================================"
+echo " CentOS 8 (PHP 7.4)"
+echo "================================================"
+echo -n "[1/4] Starting MariaDB 10.5 ..... "
+# initialize mysql
+mysql_install_db > /dev/null
+chown -R mysql:mysql /var/lib/mysql
+# run mysql server
+nohup /usr/sbin/mysqld -u mysql > /root/mysql.log 2>&1 &
+# wait for mysql to become available
+while ! mysqladmin ping -hlocalhost >/dev/null 2>&1; do
+ sleep 1
+done
+# create database and user on mysql
+mysql -u root >/dev/null << 'EOF'
+CREATE DATABASE `php-crud-api` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
+CREATE USER 'php-crud-api'@'localhost' IDENTIFIED BY 'php-crud-api';
+GRANT ALL PRIVILEGES ON `php-crud-api`.* TO 'php-crud-api'@'localhost' WITH GRANT OPTION;
+FLUSH PRIVILEGES;
+EOF
+echo "done"
+
+echo -n "[2/4] Starting PostgreSQL 12.5 .. "
+# initialize postgresql
+su - -c "/usr/pgsql-12/bin/initdb --auth-local peer --auth-host password -D /var/lib/pgsql/data" postgres > /dev/null
+# run postgres server
+nohup su - -c "/usr/pgsql-12/bin/postgres -D /var/lib/pgsql/data" postgres > /root/postgres.log 2>&1 &
+# wait for postgres to become available
+until su - -c "psql -U postgres -c '\q'" postgres >/dev/null 2>&1; do
+ sleep 1;
+done
+# create database and user on postgres
+su - -c "psql -U postgres >/dev/null" postgres << 'EOF'
+CREATE USER "php-crud-api" WITH PASSWORD 'php-crud-api';
+CREATE DATABASE "php-crud-api";
+GRANT ALL PRIVILEGES ON DATABASE "php-crud-api" to "php-crud-api";
+\c "php-crud-api";
+CREATE EXTENSION IF NOT EXISTS postgis;
+\q
+EOF
+echo "done"
+
+echo -n "[3/4] Starting SQLServer 2017 ... "
+echo "skipped"
+
+echo -n "[4/4] Cloning PHP-CRUD-API v2 ... "
+# install software
+if [ -d /php-crud-api ]; then
+ echo "skipped"
+else
+ git clone --quiet https://github.com/mevdschee/php-crud-api.git
+ echo "done"
+fi
+
+echo "------------------------------------------------"
+
+# run the tests
+cd php-crud-api
+php test.php
diff --git a/seatmap-webapi/docker/clean_all.sh b/seatmap-webapi/docker/clean_all.sh
new file mode 100755
index 0000000..c6090a9
--- /dev/null
+++ b/seatmap-webapi/docker/clean_all.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+# Delete all containers
+docker rm $(docker ps -a -q)
+# Delete all images
+docker rmi $(docker images -q)
diff --git a/seatmap-webapi/docker/debian10/Dockerfile b/seatmap-webapi/docker/debian10/Dockerfile
new file mode 100644
index 0000000..ac4e194
--- /dev/null
+++ b/seatmap-webapi/docker/debian10/Dockerfile
@@ -0,0 +1,16 @@
+FROM debian:10
+
+ARG DEBIAN_FRONTEND=noninteractive
+
+# install: php / mysql / postgres / sqlite / tools / mssql deps
+RUN apt-get update && apt-get -y install \
+php-cli php-xml php-mbstring \
+mariadb-server mariadb-client php-mysql \
+postgresql php-pgsql \
+postgresql-11-postgis-2.5 \
+sqlite3 php-sqlite3 \
+git wget
+
+# install run script
+ADD run.sh /usr/sbin/docker-run
+CMD docker-run
diff --git a/seatmap-webapi/docker/debian10/run.sh b/seatmap-webapi/docker/debian10/run.sh
new file mode 100755
index 0000000..078cabf
--- /dev/null
+++ b/seatmap-webapi/docker/debian10/run.sh
@@ -0,0 +1,58 @@
+#!/bin/bash
+echo "================================================"
+echo " Debian 10 (PHP 7.3)"
+echo "================================================"
+
+echo -n "[1/4] Starting MariaDB 10.3 ..... "
+# make sure mysql can create socket and lock
+mkdir /var/run/mysqld && chmod 777 /var/run/mysqld
+# run mysql server
+nohup mysqld > /root/mysql.log 2>&1 &
+# wait for mysql to become available
+while ! mysqladmin ping -hlocalhost >/dev/null 2>&1; do
+ sleep 1
+done
+# create database and user on mysql
+mysql -u root >/dev/null << 'EOF'
+CREATE DATABASE `php-crud-api` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
+CREATE USER 'php-crud-api'@'localhost' IDENTIFIED BY 'php-crud-api';
+GRANT ALL PRIVILEGES ON `php-crud-api`.* TO 'php-crud-api'@'localhost' WITH GRANT OPTION;
+FLUSH PRIVILEGES;
+EOF
+echo "done"
+
+echo -n "[2/4] Starting PostgreSQL 11.4 .. "
+# run postgres server
+nohup su - -c "/usr/lib/postgresql/11/bin/postgres -D /etc/postgresql/11/main" postgres > /root/postgres.log 2>&1 &
+# wait for postgres to become available
+until su - -c "psql -U postgres -c '\q'" postgres >/dev/null 2>&1; do
+ sleep 1;
+done
+# create database and user on postgres
+su - -c "psql -U postgres >/dev/null" postgres << 'EOF'
+CREATE USER "php-crud-api" WITH PASSWORD 'php-crud-api';
+CREATE DATABASE "php-crud-api";
+GRANT ALL PRIVILEGES ON DATABASE "php-crud-api" to "php-crud-api";
+\c "php-crud-api";
+CREATE EXTENSION IF NOT EXISTS postgis;
+\q
+EOF
+echo "done"
+
+echo -n "[3/4] Starting SQLServer 2017 ... "
+echo "skipped"
+
+echo -n "[4/4] Cloning PHP-CRUD-API v2 ... "
+# install software
+if [ -d /php-crud-api ]; then
+ echo "skipped"
+else
+ git clone --quiet https://github.com/mevdschee/php-crud-api.git
+ echo "done"
+fi
+
+echo "------------------------------------------------"
+
+# run the tests
+cd php-crud-api
+php test.php
diff --git a/seatmap-webapi/docker/debian9/Dockerfile b/seatmap-webapi/docker/debian9/Dockerfile
new file mode 100644
index 0000000..4d2ae9c
--- /dev/null
+++ b/seatmap-webapi/docker/debian9/Dockerfile
@@ -0,0 +1,16 @@
+FROM debian:9
+
+ARG DEBIAN_FRONTEND=noninteractive
+
+# install: php / mysql / postgres / sqlite / tools / mssql deps
+RUN apt-get update && apt-get -y install \
+php-cli php-xml php-mbstring \
+mariadb-server mariadb-client php-mysql \
+postgresql php-pgsql \
+postgresql-9.6-postgis-2.3 \
+sqlite3 php-sqlite3 \
+git wget
+
+# install run script
+ADD run.sh /usr/sbin/docker-run
+CMD docker-run
diff --git a/seatmap-webapi/docker/debian9/run.sh b/seatmap-webapi/docker/debian9/run.sh
new file mode 100755
index 0000000..9bddef7
--- /dev/null
+++ b/seatmap-webapi/docker/debian9/run.sh
@@ -0,0 +1,58 @@
+#!/bin/bash
+echo "================================================"
+echo " Debian 9 (PHP 7.0)"
+echo "================================================"
+
+echo -n "[1/4] Starting MariaDB 10.1 ..... "
+# make sure mysql can create socket and lock
+mkdir /var/run/mysqld && chmod 777 /var/run/mysqld
+# run mysql server
+nohup mysqld > /root/mysql.log 2>&1 &
+# wait for mysql to become available
+while ! mysqladmin ping -hlocalhost >/dev/null 2>&1; do
+ sleep 1
+done
+# create database and user on mysql
+mysql -u root >/dev/null << 'EOF'
+CREATE DATABASE `php-crud-api` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
+CREATE USER 'php-crud-api'@'localhost' IDENTIFIED BY 'php-crud-api';
+GRANT ALL PRIVILEGES ON `php-crud-api`.* TO 'php-crud-api'@'localhost' WITH GRANT OPTION;
+FLUSH PRIVILEGES;
+EOF
+echo "done"
+
+echo -n "[2/4] Starting PostgreSQL 9.6 ... "
+# run postgres server
+nohup su - -c "/usr/lib/postgresql/9.6/bin/postgres -D /etc/postgresql/9.6/main" postgres > /root/postgres.log 2>&1 &
+# wait for postgres to become available
+until su - -c "psql -U postgres -c '\q'" postgres >/dev/null 2>&1; do
+ sleep 1;
+done
+# create database and user on postgres
+su - -c "psql -U postgres >/dev/null" postgres << 'EOF'
+CREATE USER "php-crud-api" WITH PASSWORD 'php-crud-api';
+CREATE DATABASE "php-crud-api";
+GRANT ALL PRIVILEGES ON DATABASE "php-crud-api" to "php-crud-api";
+\c "php-crud-api";
+CREATE EXTENSION IF NOT EXISTS postgis;
+\q
+EOF
+echo "done"
+
+echo -n "[3/4] Starting SQLServer 2017 ... "
+echo "skipped"
+
+echo -n "[4/4] Cloning PHP-CRUD-API v2 ... "
+# install software
+if [ -d /php-crud-api ]; then
+ echo "skipped"
+else
+ git clone --quiet https://github.com/mevdschee/php-crud-api.git
+ echo "done"
+fi
+
+echo "------------------------------------------------"
+
+# run the tests
+cd php-crud-api
+php test.php
diff --git a/seatmap-webapi/docker/run.sh b/seatmap-webapi/docker/run.sh
new file mode 100755
index 0000000..743ccc8
--- /dev/null
+++ b/seatmap-webapi/docker/run.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+FILES=*
+i=0
+options=()
+for f in $FILES; do
+ if [[ -d "$f" ]]; then
+ ((i++))
+ options[$i]=$f
+ fi
+done
+PS3="> "
+select f in "${options[@]}"; do
+ if (( REPLY > 0 && REPLY <= ${#options[@]} )); then
+ break
+ else
+ exit
+ fi
+done
+dir=$(readlink -f ..)
+docker rm "php-crud-api_$f" > /dev/null 2>&1
+docker run -ti -v $dir:/php-crud-api --name "php-crud-api_$f" "php-crud-api:$f" /bin/bash -c '/usr/sbin/docker-run && cd php-crud-api && /bin/bash'
diff --git a/seatmap-webapi/docker/run_all.sh b/seatmap-webapi/docker/run_all.sh
new file mode 100755
index 0000000..6d5d9dc
--- /dev/null
+++ b/seatmap-webapi/docker/run_all.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+FILES=*
+
+for f in $FILES
+do
+if [[ -d "$f" ]]
+then
+ dir=$(readlink -f ..)
+ docker rm "php-crud-api_$f" > /dev/null 2>&1
+ docker run -ti -v $dir:/php-crud-api --name "php-crud-api_$f" "php-crud-api:$f"
+fi
+done
diff --git a/seatmap-webapi/docker/ubuntu16/Dockerfile b/seatmap-webapi/docker/ubuntu16/Dockerfile
new file mode 100644
index 0000000..f7f22f5
--- /dev/null
+++ b/seatmap-webapi/docker/ubuntu16/Dockerfile
@@ -0,0 +1,37 @@
+FROM ubuntu:16.04
+
+ARG DEBIAN_FRONTEND=noninteractive
+
+# install: php / mysql / postgres / tools / mssql deps
+RUN apt-get update && apt-get -y install \
+php-cli php-xml php-mbstring \
+mariadb-server mariadb-client php-mysql \
+postgresql php-pgsql \
+postgresql-9.5-postgis-2.2 \
+git wget \
+curl apt-transport-https debconf-utils sudo
+
+# adding custom MS repository
+RUN curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
+RUN curl https://packages.microsoft.com/config/ubuntu/16.04/prod.list > /etc/apt/sources.list.d/mssql-release.list
+RUN curl https://packages.microsoft.com/config/ubuntu/16.04/mssql-server-2017.list > /etc/apt/sources.list.d/mssql-server-2017.list
+
+# install SQL Server and tools
+RUN apt-get update && apt-get -y install mssql-server
+RUN ACCEPT_EULA=Y MSSQL_PID=Express MSSQL_SA_PASSWORD=sapwd123! /opt/mssql/bin/mssql-conf setup || true
+RUN ACCEPT_EULA=Y apt-get install -y msodbcsql mssql-tools
+
+# install pdo_sqlsrv
+RUN apt-get -y install php-pear build-essential unixodbc-dev php-dev
+RUN pecl install pdo_sqlsrv-5.3.0
+RUN echo extension=pdo_sqlsrv.so > /etc/php/7.0/mods-available/pdo_sqlsrv.ini
+RUN phpenmod pdo_sqlsrv
+
+# install locales
+RUN apt-get -y install locales
+RUN locale-gen en_US.UTF-8
+RUN update-locale LANG=en_US.UTF-8
+
+# install run script
+ADD run.sh /usr/sbin/docker-run
+CMD docker-run
diff --git a/seatmap-webapi/docker/ubuntu16/run.sh b/seatmap-webapi/docker/ubuntu16/run.sh
new file mode 100755
index 0000000..a4dcd84
--- /dev/null
+++ b/seatmap-webapi/docker/ubuntu16/run.sh
@@ -0,0 +1,73 @@
+#!/bin/bash
+echo "================================================"
+echo " Ubuntu 16.04 (PHP 7.0)"
+echo "================================================"
+
+echo -n "[1/4] Starting MariaDB 10.0 ..... "
+# make sure mysql can create socket and lock
+mkdir /var/run/mysqld && chmod 777 /var/run/mysqld
+# run mysql server
+nohup mysqld > /root/mysql.log 2>&1 &
+# wait for mysql to become available
+while ! mysqladmin ping -hlocalhost >/dev/null 2>&1; do
+ sleep 1
+done
+# create database and user on mysql
+mysql -u root >/dev/null << 'EOF'
+CREATE DATABASE `php-crud-api` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
+CREATE USER 'php-crud-api'@'localhost' IDENTIFIED BY 'php-crud-api';
+GRANT ALL PRIVILEGES ON `php-crud-api`.* TO 'php-crud-api'@'localhost' WITH GRANT OPTION;
+FLUSH PRIVILEGES;
+EOF
+echo "done"
+
+echo -n "[2/4] Starting PostgreSQL 9.5 ... "
+# run postgres server
+nohup su - -c "/usr/lib/postgresql/9.5/bin/postgres -D /etc/postgresql/9.5/main" postgres > /root/postgres.log 2>&1 &
+# wait for postgres to become available
+until su - -c "psql -U postgres -c '\q'" postgres >/dev/null 2>&1; do
+ sleep 1;
+done
+# create database and user on postgres
+su - -c "psql -U postgres >/dev/null" postgres << 'EOF'
+CREATE USER "php-crud-api" WITH PASSWORD 'php-crud-api';
+CREATE DATABASE "php-crud-api";
+GRANT ALL PRIVILEGES ON DATABASE "php-crud-api" to "php-crud-api";
+\c "php-crud-api";
+CREATE EXTENSION IF NOT EXISTS postgis;
+\q
+EOF
+echo "done"
+
+echo -n "[3/4] Starting SQLServer 2017 ... "
+# run sqlserver server
+nohup /opt/mssql/bin/sqlservr --accept-eula > /root/mssql.log 2>&1 &
+# create database and user on postgres
+/opt/mssql-tools/bin/sqlcmd -l 30 -S localhost -U SA -P sapwd123! >/dev/null << 'EOF'
+CREATE DATABASE [php-crud-api]
+GO
+CREATE LOGIN [php-crud-api] WITH PASSWORD=N'php-crud-api', DEFAULT_DATABASE=[php-crud-api], CHECK_EXPIRATION=OFF, CHECK_POLICY=OFF
+GO
+USE [php-crud-api]
+GO
+CREATE USER [php-crud-api] FOR LOGIN [php-crud-api] WITH DEFAULT_SCHEMA=[dbo]
+exec sp_addrolemember 'db_owner', 'php-crud-api';
+GO
+exit
+EOF
+echo "done"
+
+echo -n "[4/4] Cloning PHP-CRUD-API v2 ... "
+# install software
+if [ -d /php-crud-api ]; then
+ echo "skipped"
+else
+ git clone --quiet https://github.com/mevdschee/php-crud-api.git
+ echo "done"
+fi
+
+echo "------------------------------------------------"
+
+# run the tests
+cd php-crud-api
+php test.php
diff --git a/seatmap-webapi/docker/ubuntu18/Dockerfile b/seatmap-webapi/docker/ubuntu18/Dockerfile
new file mode 100644
index 0000000..e9c3a63
--- /dev/null
+++ b/seatmap-webapi/docker/ubuntu18/Dockerfile
@@ -0,0 +1,21 @@
+FROM ubuntu:18.04
+
+ARG DEBIAN_FRONTEND=noninteractive
+
+# install: php / mysql / postgres / sqlite / tools
+RUN apt-get update && apt-get -y install \
+php-cli php-xml php-mbstring \
+mysql-server mysql-client php-mysql \
+postgresql php-pgsql \
+postgresql-10-postgis-2.4 \
+sqlite3 php-sqlite3 \
+git wget
+
+# install locales
+RUN apt-get -y install locales
+RUN locale-gen en_US.UTF-8
+RUN update-locale LANG=en_US.UTF-8
+
+# install run script
+ADD run.sh /usr/sbin/docker-run
+CMD docker-run
diff --git a/seatmap-webapi/docker/ubuntu18/run.sh b/seatmap-webapi/docker/ubuntu18/run.sh
new file mode 100755
index 0000000..770c39c
--- /dev/null
+++ b/seatmap-webapi/docker/ubuntu18/run.sh
@@ -0,0 +1,60 @@
+#!/bin/bash
+echo "================================================"
+echo " Ubuntu 18.04 (PHP 7.2)"
+echo "================================================"
+
+echo -n "[1/4] Starting MySQL 5.7 ........ "
+# make sure mysql can create socket and lock
+mkdir /var/run/mysqld && chmod 777 /var/run/mysqld
+# run mysql server
+nohup mysqld > /root/mysql.log 2>&1 &
+# wait for mysql to become available
+while ! mysqladmin ping -hlocalhost >/dev/null 2>&1; do
+ sleep 1
+done
+# create database and user on mysql
+mysql -u root >/dev/null << 'EOF'
+CREATE DATABASE `php-crud-api` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
+CREATE USER 'php-crud-api'@'localhost' IDENTIFIED BY 'php-crud-api';
+GRANT ALL PRIVILEGES ON `php-crud-api`.* TO 'php-crud-api'@'localhost' WITH GRANT OPTION;
+FLUSH PRIVILEGES;
+EOF
+echo "done"
+
+echo -n "[2/4] Starting PostgreSQL 10.4 .. "
+# ensure statistics can be written
+mkdir /var/run/postgresql/10-main.pg_stat_tmp/ && chmod 777 /var/run/postgresql/10-main.pg_stat_tmp/
+# run postgres server
+nohup su - -c "/usr/lib/postgresql/10/bin/postgres -D /etc/postgresql/10/main" postgres > /root/postgres.log 2>&1 &
+# wait for postgres to become available
+until su - -c "psql -U postgres -c '\q'" postgres >/dev/null 2>&1; do
+ sleep 1;
+done
+# create database and user on postgres
+su - -c "psql -U postgres >/dev/null" postgres << 'EOF'
+CREATE USER "php-crud-api" WITH PASSWORD 'php-crud-api';
+CREATE DATABASE "php-crud-api";
+GRANT ALL PRIVILEGES ON DATABASE "php-crud-api" to "php-crud-api";
+\c "php-crud-api";
+CREATE EXTENSION IF NOT EXISTS postgis;
+\q
+EOF
+echo "done"
+
+echo -n "[3/4] Starting SQLServer 2017 ... "
+echo "skipped"
+
+echo -n "[4/4] Cloning PHP-CRUD-API v2 ... "
+# install software
+if [ -d /php-crud-api ]; then
+ echo "skipped"
+else
+ git clone --quiet https://github.com/mevdschee/php-crud-api.git
+ echo "done"
+fi
+
+echo "------------------------------------------------"
+
+# run the tests
+cd php-crud-api
+php test.php
diff --git a/seatmap-webapi/docker/ubuntu20/Dockerfile b/seatmap-webapi/docker/ubuntu20/Dockerfile
new file mode 100644
index 0000000..b311134
--- /dev/null
+++ b/seatmap-webapi/docker/ubuntu20/Dockerfile
@@ -0,0 +1,21 @@
+FROM ubuntu:20.04
+
+ARG DEBIAN_FRONTEND=noninteractive
+
+# install: php / mysql / postgres / sqlite / tools
+RUN apt-get update && apt-get -y install \
+php-cli php-xml php-mbstring \
+mysql-server mysql-client php-mysql \
+postgresql php-pgsql \
+postgresql-12-postgis-3 \
+sqlite3 php-sqlite3 \
+git wget
+
+# install locales
+RUN apt-get -y install locales
+RUN locale-gen en_US.UTF-8
+RUN update-locale LANG=en_US.UTF-8
+
+# install run script
+ADD run.sh /usr/sbin/docker-run
+CMD docker-run
diff --git a/seatmap-webapi/docker/ubuntu20/run.sh b/seatmap-webapi/docker/ubuntu20/run.sh
new file mode 100755
index 0000000..3ca98b0
--- /dev/null
+++ b/seatmap-webapi/docker/ubuntu20/run.sh
@@ -0,0 +1,60 @@
+#!/bin/bash
+echo "================================================"
+echo " Ubuntu 20.04 (PHP 7.4)"
+echo "================================================"
+
+echo -n "[1/4] Starting MySQL 8.0 ........ "
+# make sure mysql can create socket and lock
+mkdir /var/run/mysqld && chmod 777 /var/run/mysqld
+# run mysql server
+nohup mysqld > /root/mysql.log 2>&1 &
+# wait for mysql to become available
+while ! mysqladmin ping -hlocalhost >/dev/null 2>&1; do
+ sleep 1
+done
+# create database and user on mysql
+mysql -u root >/dev/null << 'EOF'
+CREATE DATABASE `php-crud-api` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
+CREATE USER 'php-crud-api'@'localhost' IDENTIFIED WITH MYSQL_NATIVE_PASSWORD BY 'php-crud-api';
+GRANT ALL PRIVILEGES ON `php-crud-api`.* TO 'php-crud-api'@'localhost' WITH GRANT OPTION;
+FLUSH PRIVILEGES;
+EOF
+echo "done"
+
+echo -n "[2/4] Starting PostgreSQL 12.2 .. "
+# ensure statistics can be written
+mkdir /var/run/postgresql/10-main.pg_stat_tmp/ && chmod 777 /var/run/postgresql/10-main.pg_stat_tmp/
+# run postgres server
+nohup su - -c "/usr/lib/postgresql/12/bin/postgres -D /etc/postgresql/12/main" postgres > /root/postgres.log 2>&1 &
+# wait for postgres to become available
+until su - -c "psql -U postgres -c '\q'" postgres >/dev/null 2>&1; do
+ sleep 1;
+done
+# create database and user on postgres
+su - -c "psql -U postgres >/dev/null" postgres << 'EOF'
+CREATE USER "php-crud-api" WITH PASSWORD 'php-crud-api';
+CREATE DATABASE "php-crud-api";
+GRANT ALL PRIVILEGES ON DATABASE "php-crud-api" to "php-crud-api";
+\c "php-crud-api";
+CREATE EXTENSION IF NOT EXISTS postgis;
+\q
+EOF
+echo "done"
+
+echo -n "[3/4] Starting SQLServer 2017 ... "
+echo "skipped"
+
+echo -n "[4/4] Cloning PHP-CRUD-API v2 ... "
+# install software
+if [ -d /php-crud-api ]; then
+ echo "skipped"
+else
+ git clone --quiet https://github.com/mevdschee/php-crud-api.git
+ echo "done"
+fi
+
+echo "------------------------------------------------"
+
+# run the tests
+cd php-crud-api
+php test.php
diff --git a/seatmap-webapi/examples/clients/angular.html b/seatmap-webapi/examples/clients/angular.html
new file mode 100644
index 0000000..b14fb1f
--- /dev/null
+++ b/seatmap-webapi/examples/clients/angular.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+