Merge commit '1392bd3a96045302b60d845a90901a4b2234c475' as 'seatmap-webapi'

This commit is contained in:
zino
2021-01-20 12:59:59 +01:00
261 changed files with 39680 additions and 0 deletions

3
seatmap-webapi/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
composer.phar
vendor/
data.db

5
seatmap-webapi/.htaccess Normal file
View File

@@ -0,0 +1,5 @@
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ api.php/$1 [QSA,L]
</IfModule>

View File

@@ -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).

15
seatmap-webapi/Dockerfile Normal file
View File

@@ -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

21
seatmap-webapi/LICENSE Normal file
View File

@@ -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.

1389
seatmap-webapi/README.md Normal file

File diff suppressed because it is too large Load Diff

11387
seatmap-webapi/api.include.php Normal file

File diff suppressed because it is too large Load Diff

11410
seatmap-webapi/api.php Normal file

File diff suppressed because it is too large Load Diff

113
seatmap-webapi/build.php Normal file
View File

@@ -0,0 +1,113 @@
<?php
// combine src and vendor directories into a single file
function removeIgnored(string $dir, array &$entries, array $ignore)
{
foreach ($entries as $i => $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 = <<<EOF
<?php
/**
* PHP-CRUD-API v2 License: MIT
* Maurits van der Schee: maurits@vdschee.nl
* https://github.com/mevdschee/php-crud-api
*
* Dependencies:
* - vendor/psr/*: PHP-FIG
* https://github.com/php-fig
* - vendor/nyholm/*: Tobias Nyholm
* https://github.com/Nyholm
**/
EOF;
foreach (explode("\n", $head) as $line) {
array_push($lines, $line);
}
}
function run(string $base, array $dirs, string $filename, array $ignore)
{
$lines = [];
$start = microtime(true);
addHeader($lines);
$ignore = array_flip($ignore);
$count = 0;
foreach ($dirs as $dir) {
$count += runDir($base, $dir, $lines, $ignore);
}
$data = implode("\n", $lines);
$data = preg_replace('/\n({)?\s*\n\s*\n/', "\n$1\n", $data);
file_put_contents('tmp_' . $filename, $data);
ob_start();
include 'tmp_' . $filename;
ob_end_clean();
rename('tmp_' . $filename, $filename);
$data = substr($data, 0, strrpos($data, "\n// file: src/index.php"));
file_put_contents(str_replace('.php', '.include.php', $filename), $data);
$end = microtime(true);
$time = ($end - $start) * 1000;
echo sprintf("%d files combined in %d ms into '%s'\n", $count, $time, $filename);
}
$ignore = [
'vendor/nyholm/psr7/src/Factory/HttplugFactory.php',
];
$directories = ['vendor/nyholm', 'src'];
if (!extension_loaded('psr')) {
array_unshift($directories, 'vendor/psr');
}
run(__DIR__, $directories, 'api.php', $ignore);

View File

@@ -0,0 +1,54 @@
{
"name": "mevdschee/php-crud-api",
"type": "library",
"description": "Single file PHP script that adds a REST API to a SQL database.",
"keywords": [
"api-server",
"restful",
"mysql",
"geospatial",
"php",
"sql-server",
"postgresql",
"php-api",
"postgis",
"crud",
"rest-api",
"openapi",
"swagger",
"automatic-api",
"database",
"multi-database",
"sql-database",
"ubuntu-linux"
],
"homepage": "https://github.com/mevdschee/php-crud-api",
"license": "MIT",
"authors": [
{
"name": "Maurits van der Schee",
"email": "maurits@vdschee.nl",
"homepage": "https://github.com/mevdschee"
}
],
"require": {
"php": ">=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" }
}
}

396
seatmap-webapi/composer.lock generated Normal file
View File

@@ -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"
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,5 @@
#!/bin/bash
# Delete all containers
docker rm $(docker ps -a -q)
# Delete all images
docker rmi $(docker images -q)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

21
seatmap-webapi/docker/run.sh Executable file
View File

@@ -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'

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,24 @@
<html>
<head>
<script src= "http://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
<script>
var app = angular.module('myApplication', []);
app.controller('postController', function($scope, $http) {
var url = '/api.php/records/posts';
$http.post(url, {user_id: 1, category_id: 1, content: "from angular"}).success(function() {
$http.get(url).success(function(response) {
$scope.posts = response.records;
});
});
});
</script>
</head>
<body>
<div ng-app="myApplication" ng-controller="postController">
<ul>
<li ng-repeat="x in posts">{{ x.id + ', ' + x.content }}</li>
</ul>
</div>
</body>
</html>

View File

@@ -0,0 +1,31 @@
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/2.0.0-beta.14/angular2-polyfills.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/2.0.0-beta.14/Rx.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/2.0.0-beta.14/angular2-all.umd.min.js"></script>
<script>
AppComponent =
ng.core.Component({
selector: 'my-app',
providers: [ng.http.HTTP_PROVIDERS],
template: '<ul><li *ngFor="#x of posts">{{ x.id + ", " + x.content }}</li></ul>'
})
.Class({
constructor: [
ng.http.Http, function(http) {
var url = "/api.php/records/posts";
http.post(url,JSON.stringify({user_id:1,category_id:1,content:"from angular2"})).subscribe();
http.get(url).map(res => res.json()).subscribe(res => this.posts = res.records);
}
]
});
document.addEventListener("DOMContentLoaded", function(event) {
ng.core.enableProdMode();
ng.platform.browser.bootstrap(AppComponent);
});
</script>
</head>
<body>
<my-app>Loading...</my-app>
</body>
</html>

View File

@@ -0,0 +1,33 @@
<html>
<head>
<meta charset="utf-8" />
<script>
var authUrl = 'auth.php'; // url of 'auth.php' from php-api-auth
var clientId = 'default'; // client id as defined in php-api-auth
var audience = 'api.php'; // api audience as defined in php-api-auth
window.onload = function () {
var match = RegExp('[#&]access_token=([^&]*)').exec(window.location.hash);
var accessToken = match && decodeURIComponent(match[1].replace(/\+/g, ' '));
if (!accessToken) {
document.location = authUrl+'?audience='+audience+'&response_type=token&client_id='+clientId+'&redirect_uri='+document.location.href;
} else {
document.location.hash = '';
var req = new XMLHttpRequest();
req.onreadystatechange = function () {
if (req.readyState==4) {
console.log(req.responseText);
document.getElementById('output').innerHTML = JSON.stringify(JSON.parse(req.responseText), undefined, 4);
}
}
url = 'api.php/records/posts?join=categories&join=tags&join=comments&filter=id,eq,1';
req.open("GET", url, true);
req.setRequestHeader('X-Authorization', 'Bearer '+accessToken);
req.send();
}
};
</script>
</head>
<body>
<pre id="output"></pre>
</body>
</html>

View File

@@ -0,0 +1,49 @@
<html>
<head>
<meta charset="utf-8" />
<script>
var authUrl = 'https://php-crud-api.auth0.com/authorize'; // url of auth0 '/authorize' end-point
var clientId = ''; // client id as defined in auth0
var audience = 'https://your-php-crud-api/api.php'; // api audience as defined in auth0
var url = '/api.php/records/posts?join=categories&join=tags&join=comments&filter=id,eq,1';
function requestAPI() {
var match = RegExp('[#&]access_token=([^&]*)').exec(window.location.hash);
var accessToken = match && decodeURIComponent(match[1].replace(/\+/g, ' '));
if (!accessToken) {
document.location = authUrl+'?audience='+audience+'&response_type=token&client_id='+clientId+'&redirect_uri='+document.location.href;
} else {
document.location.hash = '';
var req = new XMLHttpRequest();
req.onreadystatechange = function () {
if (req.readyState==4) {
console.log(req.responseText);
try {
document.getElementById('output').innerHTML = JSON.stringify(JSON.parse(req.responseText), undefined, 4);
} catch (error) {
document.getElementById('output').innerHTML = req.responseText;
}
}
}
req.open("GET", url, true);
req.setRequestHeader('X-Authorization', 'Bearer '+accessToken);
req.send();
}
};
window.onload = function() {
requestAPI()
document.getElementById('request-btn').onclick = function(e) {
e.preventDefault()
requestAPI()
}
}
</script>
</head>
<body>
<p>
<button type="button" id="request-btn">Request API</button>
</p>
<pre id="output"></pre>
</body>
</html>

View File

@@ -0,0 +1,45 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.10.20/css/jquery.dataTables.min.css"/>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.3.1.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js"></script>
<script>
$(document).ready(function() {
$('#example').DataTable( {
ajax: {
url: '/api.php/records/posts?join=categories&join=users',
dataSrc: 'records'
},
columns: [
{ data: "id" },
{ data: "user_id.username" },
{ data: "category_id.name" },
{ data: "content" }
]
});
});
</script>
</head>
<body>
<table id="example" class="display" style="width:100%">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Category</th>
<th>Content</th>
</tr>
</thead>
<tfoot>
<tr>
<th>ID</th>
<th>Username</th>
<th>Category</th>
<th>Content</th>
</tr>
</tfoot>
</table>
</body>
</html>

View File

@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Success</title>
<script src="https://www.gstatic.com/firebasejs/6.0.2/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/6.0.2/firebase-auth.js"></script>
<script>
// Your web app's Firebase configuration
var firebaseConfig = {
apiKey: "",
authDomain: "",
databaseURL: "",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: ""
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
</script>
<script>
var url = '/api.php/records/posts?join=categories&join=tags&join=comments&filter=id,eq,1';
function requestAPI(accessToken) {
var req = new XMLHttpRequest();
req.onreadystatechange = function () {
if (req.readyState == 4) {
try {
document.getElementById('output').innerHTML = JSON.stringify(JSON.parse(req.responseText),
undefined, 4);
} catch (error) {
document.getElementById('output').innerHTML = req.responseText;
}
}
}
req.open("GET", url, true);
req.setRequestHeader('X-Authorization', 'Bearer ' + accessToken);
req.send();
}
function initApp() {
firebase.auth().onAuthStateChanged(function (user) {
if (user) {
// User is signed in.
var displayName = user.displayName;
var email = user.email;
var emailVerified = user.emailVerified;
var photoURL = user.photoURL;
var uid = user.uid;
var phoneNumber = user.phoneNumber;
var providerData = user.providerData;
user.getIdToken().then(function (accessToken) {
document.getElementById('sign-in-status').textContent = 'Signed in';
document.getElementById('account-details').textContent = JSON.stringify({
displayName: displayName,
email: email,
emailVerified: emailVerified,
phoneNumber: phoneNumber,
photoURL: photoURL,
uid: uid,
accessToken: accessToken,
providerData: providerData
}, undefined, 4);
requestAPI(accessToken)
});
} else {
// User is signed out.
document.getElementById('sign-in-status').textContent = 'Signed out';
document.getElementById('account-details').textContent = 'null';
}
}, function (error) {
console.log(error);
});
};
window.addEventListener('load', initApp);
</script>
</head>
<body>
<h1>Firebase Login Success (or not)</h1>
<div id="sign-in-status"></div>
<pre id="account-details"></pre>
<pre id="output"></pre>
</body>
</html>

View File

@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>
Firebase
</title>
<!-- The core Firebase JS SDK is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/6.0.2/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/6.0.2/firebase-auth.js"></script>
<script>
// Your web app's Firebase configuration
var firebaseConfig = {
apiKey: "",
authDomain: "",
databaseURL: "",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: ""
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
</script>
<script src="https://cdn.firebase.com/libs/firebaseui/4.0.0/firebaseui.js"></script>
<link type="text/css" rel="stylesheet" href="https://cdn.firebase.com/libs/firebaseui/4.0.0/firebaseui.css" />
<script type="text/javascript">
// FirebaseUI config.
var uiConfig = {
signInSuccessUrl: './vanilla-success.html',
signInOptions: [
// Leave the lines as is for the providers you want to offer your users.
firebase.auth.GoogleAuthProvider.PROVIDER_ID,
firebase.auth.FacebookAuthProvider.PROVIDER_ID,
firebase.auth.TwitterAuthProvider.PROVIDER_ID,
firebase.auth.GithubAuthProvider.PROVIDER_ID,
firebase.auth.EmailAuthProvider.PROVIDER_ID,
firebase.auth.PhoneAuthProvider.PROVIDER_ID,
firebaseui.auth.AnonymousAuthProvider.PROVIDER_ID
],
// tosUrl and privacyPolicyUrl accept either url string or a callback
// function.
// Terms of service url/callback.
tosUrl: '<your-tos-url>',
// Privacy policy url/callback.
privacyPolicyUrl: function() {
window.location.assign('<your-privacy-policy-url>');
}
};
// Initialize the FirebaseUI Widget using Firebase.
var ui = new firebaseui.auth.AuthUI(firebase.auth());
// The start method will wait until the DOM is loaded.
ui.start('#firebaseui-auth-container', uiConfig);
</script>
</head>
<body>
<h1>Firebase login</h1>
<div id="firebaseui-auth-container"></div>
</body>
</html>

View File

@@ -0,0 +1,66 @@
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.5/handlebars.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zepto/1.1.6/zepto.min.js"></script>
<script id="PostListTemplate" type="text/mustache">
<ul>
{{#records}}
<li>
<span class="id">{{id}}</span>, <span class="content">{{content}}</span>
<a href="javascript:void(0)" class="edit">edit</a>
<a href="javascript:void(0)" class="delete">del</a>
</li>
{{/records}}
<li>
<form>
<input name="content"/>
</form>
</li>
</ul>
</script>
<script>
function PostList(element, template) {
var self = this;
var url = '/api.php/records/posts';
self.edit = function() {
var li = $(this).parent('li');
var id = li.find('span.id').text();
var content = li.find('span.content').text();
content = prompt('Value',content);
if (content!==null) {
$.ajax({url:url+'/'+id, type: 'PUT', data: {content:content}, success:self.update});
}
};
self.delete = function() {
var li = $(this).parent('li');
var id = li.find('span.id').text();
if (confirm("Deleting #"+id+". Continue?")) {
$.ajax({url:url+'/'+id, type: 'DELETE', success:self.update});
}
};
self.submit = function(e) {
e.preventDefault();
var content = $(this).find('input[name="content"]').val();
$.post(url, {user_id:1,category_id:1,content:content}, self.update);
};
self.render = function(data) {
element.html(Handlebars.compile(template.html())(data));
};
self.update = function() {
$.get(url, self.render);
};
self.post = function() {
$.post(url, {user_id:1,category_id:1,content:"from handlebars"}, self.update);
};
element.on('submit','form',self.submit);
element.on('click','a.edit',self.edit)
element.on('click','a.delete',self.delete)
self.post();
};
$(function(){ new PostList($('#PostListDiv'),$('#PostListTemplate')); });
</script>
</head>
<body>
<div id="PostListDiv">Loading...</div>
</body>
</html>

View File

@@ -0,0 +1,70 @@
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zepto/1.1.6/zepto.min.js"></script>
<script id="posts-template" type="text/html">
<ul>
<!-- ko foreach: posts -->
<li>
<span data-bind="text: id"></span>, <span data-bind="text: content"></span>
<a href="javascript:void(0)" data-bind="click: $parent.edit">edit</a>
<a href="javascript:void(0)" data-bind="click: $parent.delete">del</a>
</li>
<!-- /ko -->
<li>
<form data-bind="submit: submit">
<input name="content" data-bind="value: form"/>
</form>
</li>
</ul>
</script>
<script>
var url = '/api.php/records/posts';
function Post(id,content){
var self = this;
self.id = ko.observable(id);
self.content = ko.observable(content);
}
function PostList(){
var self = this;
self.posts = ko.observableArray([]);
self.form = ko.observable('');
self.bound = false;
self.edit = function(post) {
var content = prompt('Value',post.content());
if (content!==null) {
$.ajax({url:url+'/'+post.id(), type: 'PUT', data: {content:content}, success:self.update});
}
};
self.delete = function(post) {
if (confirm("Deleting #"+post.id()+". Continue?")) {
$.ajax({url:url+'/'+post.id(), type: 'DELETE', success:self.update});
}
};
self.submit = function(form) {
$.post(url, {user_id:1,category_id:1,content:self.form()}, self.update);
};
self.render = function(data) {
var array = data.records;
self.posts.removeAll();
for (i=0;i<array.length;i++) {
self.posts.push(new Post(array[i].id,array[i].content));
}
self.form('');
if (!self.bound){ ko.applyBindings(self); self.bound = true; }
};
self.update = function() {
$.get(url, self.render);
};
self.post = function() {
$.post(url, {user_id:1,category_id:1,content:"from knockout"}, self.update);
}
self.post();
};
$(function(){ new PostList(); });
</script>
</head>
<body>
<div data-bind="template: { name: 'posts-template'}">Loading...</div>
</body>
</html>

View File

@@ -0,0 +1,85 @@
/* global L */
(function() {
L.GeoJSONLayer = L.GeoJSON.extend({
includes: L.Evented.prototype,
url: null,
map: null,
//
// Leaflet layer methods
//
initialize(url, options) {
this.url = url;
L.GeoJSON.prototype.initialize.call(this, [], options);
},
onAdd(map) {
L.GeoJSON.prototype.onAdd.call(this, map);
this.map = map;
map.on('moveend zoomend refresh', this._reloadMap, this);
this._reloadMap();
},
onRemove(map) {
map.off('moveend zoomend refresh', this._reloadMap, this);
this.map = null;
L.GeoJSON.prototype.onRemove.call(this, map);
},
//
// Custom methods
//
_reloadMap: function() {
if (this.map) {
var url = this._expandUrl(this.url);
this._ajaxRequest('GET', url, false, this._updateLayers.bind(this));
}
},
_expandUrl: function(template) {
var bbox = this.map.getBounds();
var southWest = bbox.getSouthWest();
var northEast = bbox.getNorthEast();
var bboxStr = bbox.toBBoxString();
var coords = {
lat1: southWest.lat,
lon1: southWest.lng,
lat2: northEast.lat,
lon2: northEast.lng,
bbox: bboxStr
};
return L.Util.template(template, coords);
},
_ajaxRequest: function(method, url, data, callback) {
var request = new XMLHttpRequest();
request.open(method, url, true);
request.onreadystatechange = function() {
if (request.readyState === 4 && request.status === 200) {
callback(JSON.parse(request.responseText));
}
};
if (data) {
request.setRequestHeader('Content-type', 'application/json');
request.send(JSON.stringify(data));
} else {
request.send();
}
return request;
},
_updateLayers: function(geoData) {
this.clearLayers();
this.addData(geoData);
}
});
L.geoJSONLayer = function (url, options) {
return new L.GeoJSONLayer(url, options);
};
})();

View File

@@ -0,0 +1,144 @@
/* global L */
(function() {
L.GeoJSONTileLayer = L.GridLayer.extend({
includes: L.Evented.prototype,
url: null,
map: null,
layer: null,
features: null,
cache: null,
//
// Leaflet layer methods
//
initialize(url, options) {
this.url = url;
this.layer = new L.GeoJSON(null, options);
this.features = {};
this.cache = {};
L.GridLayer.prototype.initialize.call(this, options);
},
createTile(coords, done) {
var tile = L.DomUtil.create('div', 'leaflet-tile');
tile.style['box-shadow'] = 'inset 0 0 2px #f00';
var url = this._expandUrl(this.url, coords);
if (this.cache[coords]) {
done.call(this);
} else {
this._ajaxRequest('GET', url, false, this._updateCache.bind(this, done, coords));
}
return tile;
},
onAdd(map) {
L.GridLayer.prototype.onAdd.call(this, map);
map.addLayer(this.layer);
this.map = map;
map.on('zoomanim', this._onZoomAnim.bind(this));
this.on('loading', this._onLoading.bind(this));
this.on('tileload', this._onTileLoad.bind(this));
this.on('tileunload', this._onTileUnLoad.bind(this));
},
onRemove(map) {
this.off('tileunload', this._onTileUnLoad.bind(this));
this.off('tileload', this._onTileLoad.bind(this));
this.off('loading', this._onLoading.bind(this));
map.off('zoomanim', this._onZoomAnim.bind(this));
this.map = null;
map.removeLayer(this.layer)
L.GridLayer.prototype.onRemove.call(this, map);
},
//
// Custom methods
//
_expandUrl: function(template, coords) {
return L.Util.template(template, coords);
},
_updateTiles: function() {
this.layer.clearLayers();
this.features = {};
for (var coords in this.cache) {
if (this.cache.hasOwnProperty(coords)) {
this._drawTile(coords);
}
}
},
_drawTile(coords) {
var geoData = this.cache[coords];
if (geoData.type == 'FeatureCollection'){
geoData = geoData.features;
}
for (var i=0;i<geoData.length;i++) {
var id = geoData[i].id;
if (!this.features[id]) {
this.layer.addData(geoData[i]);
this.features[id] = true;
}
}
if (!this.cache[coords]) {
this.cache[coords] = geoData;
}
},
_updateCache: function(done, coords, geoData) {
this.cache[coords] = geoData;
done.call(this);
},
_ajaxRequest: function(method, url, data, callback) {
var request = new XMLHttpRequest();
request.open(method, url, true);
request.onreadystatechange = function() {
if (request.readyState === 4 && request.status === 200) {
callback(JSON.parse(request.responseText));
}
};
if (data) {
request.setRequestHeader('Content-type', 'application/json');
request.send(JSON.stringify(data));
} else {
request.send();
}
return request;
},
_onZoomAnim: function (e) {
var zoom = e.zoom;
if ((this.options.maxZoom && zoom > this.options.maxZoom) ||
(this.options.minZoom && zoom < this.options.minZoom)) {
this.map.removeLayer(this.layer);
this.cache = {};
this.layer.clearLayers();
} else {
this._updateTiles();
this.map.addLayer(this.layer);
}
},
_onLoading: function (e) {
this._updateTiles();
},
_onTileLoad: function (e) {
this._drawTile(e.coords);
},
_onTileUnLoad: function (e) {
delete this.cache[e.coords]
},
});
L.geoJSONTileLayer = function (url, options) {
return new L.GeoJSONTileLayer(url, options);
};
})();

View File

@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html>
<head>
<title>Quick Start - Leaflet</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" type="image/x-icon" href="docs/images/favicon.ico" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js"></script>
<script src="geojson-layer.js"></script>
<script src="geojson-tile-layer.js"></script>
</head>
<body>
<div id="mapid" style="width: 600px; height: 400px;"></div>
<script>
var mymap = L.map('mapid').setView([20, 30], 3);
L.tileLayer('https://services.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', {
maxZoom: 18,
}).addTo(mymap);
L.geoJSONLayer('http://localhost:8000/api.php/geojson/users?bbox={bbox}', {
maxZoom: 18,
}).addTo(mymap);
L.geoJSONTileLayer('http://localhost:8000/src/geojson/countries?filter=id,lt,3&tile={z},{x},{y}', {
minZoom: 3,
maxZoom: 18,
}).addTo(mymap);
</script>
</body>
</html>

View File

@@ -0,0 +1,66 @@
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.2.1/mustache.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zepto/1.1.6/zepto.min.js"></script>
<script id="PostListTemplate" type="text/mustache">
<ul>
{{#records}}
<li>
<span class="id">{{id}}</span>, <span class="content">{{content}}</span>
<a href="javascript:void(0)" class="edit">edit</a>
<a href="javascript:void(0)" class="delete">del</a>
</li>
{{/records}}
<li>
<form>
<input name="content"/>
</form>
</li>
</ul>
</script>
<script>
function PostList(element, template) {
var self = this;
var url = '/api.php/records/posts';
self.edit = function() {
var li = $(this).parent('li');
var id = li.find('span.id').text();
var content = li.find('span.content').text();
content = prompt('Value',content);
if (content!==null) {
$.ajax({url:url+'/'+id, type: 'PUT', data: {content:content}, success:self.update});
}
};
self.delete = function() {
var li = $(this).parent('li');
var id = li.find('span.id').text();
if (confirm("Deleting #"+id+". Continue?")) {
$.ajax({url:url+'/'+id, type: 'DELETE', success:self.update});
}
};
self.submit = function(e) {
e.preventDefault();
var content = $(this).find('input[name="content"]').val();
$.post(url, {user_id:1,category_id:1,content:content}, self.update);
};
self.render = function(data) {
element.html(Mustache.to_html(template.html(),data));
};
self.update = function() {
$.get(url, self.render);
};
self.post = function() {
$.post(url, {user_id:1,category_id:1,content:"from mustache"}, self.update);
};
element.on('submit','form',self.submit);
element.on('click','a.edit',self.edit)
element.on('click','a.delete',self.delete)
self.post();
};
$(function(){ new PostList($('#PostListDiv'),$('#PostListTemplate')); });
</script>
</head>
<body>
<div id="PostListDiv">Loading...</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

View File

@@ -0,0 +1,47 @@
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.0/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.0/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zepto/1.1.6/zepto.min.js"></script>
<script>
var PostList = React.createClass({
displayName: 'PostList',
url: '/api.php/records/posts',
getInitialState: function() {
return { records: [] };
},
retrieveServerState: function() {
this.serverRequest = $.get(this.url, function (data) {
this.setState(data);
}.bind(this));
},
componentDidMount: function() {
$.post(this.url, {user_id:1,category_id:1,content:"from react"}, this.retrieveServerState);
},
componentWillUnmount: function() {
this.serverRequest.abort();
},
render: function render() {
var createPost = function(post) {
return React.createElement(
'li',
{key: post.id},
post.id,
', ',
post.content
);
};
return React.createElement(
'ul',
null,
this.state.records.map(createPost)
);
}
});
$(function(){ ReactDOM.render(React.createElement(PostList, null), document.getElementById('myApplication')); });
</script>
</head>
<body>
<div id="myApplication">Loading...</div>
</body>
</html>

View File

@@ -0,0 +1,73 @@
<html>
<head>
<meta charset="utf-8" />
<script>
if (!window.File || !window.FileReader || !window.FileList || !window.Blob) {
alert("Your browser is too old to support HTML5 File API");
}
function showImagePreview() {
var demoImage = document.querySelector('img#preview');
var file = document.querySelector('input[type=file]').files[0];
var reader = new FileReader();
reader.onload = function (event) {
console.log(reader.result)
demoImage.src = reader.result;
}
console.log(file)
reader.readAsDataURL(file);
}
function uploadImageFile() {
var demoImage = document.querySelector('img#preview');
var req = new XMLHttpRequest();
req.onreadystatechange = function () {
if (req.readyState==4) {
console.log(req.responseText);
listImageFiles();
}
}
url = '/api.php/records/categories';
req.open("POST", url);
var icon = demoImage.src.split(";")[1].split(",")[1];
req.send(JSON.stringify({"name":"upload","icon":icon}));
}
function listImageFiles() {
var ul = document.querySelector('ul');
var req = new XMLHttpRequest();
req.onreadystatechange = function () {
if (req.readyState==4) {
console.log(req.responseText);
output.innerHTML = "";
var categories = JSON.parse(req.responseText).records;
for (var i=0;i<categories.length;i++) {
var li = document.createElement('li');
var img = document.createElement('img');
var span = document.createElement('span');
img.style = 'height:2em;margin:0 .5em;';
img.src = "data:image/png;base64," + categories[i].icon;
span.innerHTML = categories[i].name;
li.appendChild(img);
li.appendChild(span);
ul.appendChild(li);
}
}
}
url = '/api.php/records/categories';
req.open("GET", url);
req.send();
}
</script>
</head>
<body onload="listImageFiles()">
<ul id="output"></ul>
<hr>
<form onsubmit="uploadImageFile(); return false;">
<img src="" id="preview" style="height:2em;margin:0 .5em;" alt="PNG preview..."><br><br>
<input type="file" onchange="showImagePreview()" accept="image/png"><br><br>
<input type="submit" value="Upload PNG">
</form>
</body>
</html>

View File

@@ -0,0 +1,20 @@
<html>
<head>
<script>
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
console.log(this.responseText);
jsonObject = JSON.parse(this.responseText);
document.getElementById('output').innerHTML = JSON.stringify(jsonObject, undefined, 4);
}
};
xhttp.open("GET", "/api.php/records/posts?join=categories&join=tags&join=comments&filter=id,eq,1", true);
xhttp.send();
</script>
</head>
<body>
<pre id="output"></pre>
</body>
</html>

View File

@@ -0,0 +1,250 @@
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Vue.js CRUD application</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/2.2.1/vue-router.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.15.3/axios.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap-theme.min.css">
<style>
.logo {
width: 50px;
float: left;
margin-right: 15px;
}
.form-group {
max-width: 500px;
}
.actions {
padding: 10px 0;
}
.glyphicon-euro {
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<header class="page-header">
<div class="branding">
<img src="https://vuejs.org/images/logo.png" alt="Logo" title="Home page" class="logo"/>
<h1>Vue.js CRUD application</h1>
</div>
</header>
<main id="app">
<router-view></router-view>
</main>
</div>
<template id="post-list">
<div>
<div class="actions">
<router-link class="btn btn-default" v-bind:to="{path: '/add-post'}">
<span class="glyphicon glyphicon-plus"></span>
Add post
</router-link>
</div>
<div class="filters row">
<div class="form-group col-sm-3">
<label for="search-element">Filter</label>
<input v-model="searchKey" class="form-control" id="search-element" required/>
</div>
</div>
<table class="table">
<thead>
<tr>
<th>Content</th>
<th class="col-sm-2">Actions</th>
</tr>
</thead>
<tbody>
<tr v-if="posts===null">
<td colspan="4">Loading...</td>
</tr>
<tr v-else v-for="post in filteredposts">
<td>
<router-link v-bind:to="{name: 'post', params: {post_id: post.id}}">{{ post.content }}</router-link>
</td>
<td>
<router-link class="btn btn-warning btn-xs" v-bind:to="{name: 'post-edit', params: {post_id: post.id}}">Edit</router-link>
<router-link class="btn btn-danger btn-xs" v-bind:to="{name: 'post-delete', params: {post_id: post.id}}">Delete</router-link>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<template id="add-post">
<div>
<h2>Add new post</h2>
<form v-on:submit="createpost">
<div class="form-group">
<label for="add-content">Content</label>
<textarea class="form-control" id="add-content" rows="10" v-model="post.content"></textarea>
</div>
<button type="submit" class="btn btn-primary">Create</button>
<router-link class="btn btn-default" v-bind:to="'/'">Cancel</router-link>
</form>
</div>
</template>
<template id="post">
<div>
<b>Content: </b>
<div>{{ post.content }}</div>
<br/>
<span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span>
<router-link v-bind:to="'/'">Back to post list</router-link>
</div>
</template>
<template id="post-edit">
<div>
<h2>Edit post</h2>
<form v-on:submit="updatepost">
<div class="form-group">
<label for="edit-content">Content</label>
<textarea class="form-control" id="edit-content" rows="3" v-model="post.content"></textarea>
</div>
<button type="submit" class="btn btn-primary">Save</button>
<router-link class="btn btn-default" v-bind:to="'/'">Cancel</router-link>
</form>
</div>
</template>
<template id="post-delete">
<div>
<h2>Delete post {{ post.id }}</h2>
<form v-on:submit="deletepost">
<p>The action cannot be undone.</p>
<button type="submit" class="btn btn-danger">Delete</button>
<router-link class="btn btn-default" v-bind:to="'/'">Cancel</router-link>
</form>
</div>
</template>
<script>
var posts = null;
var api = axios.create({
baseURL: '/api.php/records'
});
function findpost (postId) {
return posts[findpostKey(postId)];
};
function findpostKey (postId) {
for (var key = 0; key < posts.length; key++) {
if (posts[key].id == postId) {
return key;
}
}
};
var List = Vue.extend({
template: '#post-list',
data: function () {
return {posts: posts, searchKey: ''};
},
created: function () {
var self = this;
api.get('/posts').then(function (response) {
posts = self.posts = response.data.records;
}).catch(function (error) {
console.log(error);
});
},
computed: {
filteredposts: function () {
return this.posts.filter(function (post) {
return this.searchKey=='' || post.content.indexOf(this.searchKey) !== -1;
},this);
}
}
});
var post = Vue.extend({
template: '#post',
data: function () {
return {post: findpost(this.$route.params.post_id)};
}
});
var postEdit = Vue.extend({
template: '#post-edit',
data: function () {
return {post: findpost(this.$route.params.post_id)};
},
methods: {
updatepost: function () {
var post = this.post;
api.put('/posts/'+post.id,post).then(function (response) {
console.log(response.data);
}).catch(function (error) {
console.log(error);
});
router.push('/');
}
}
});
var postDelete = Vue.extend({
template: '#post-delete',
data: function () {
return {post: findpost(this.$route.params.post_id)};
},
methods: {
deletepost: function () {
var post = this.post;
api.delete('/posts/'+post.id).then(function (response) {
console.log(response.data);
}).catch(function (error) {
console.log(error);
});
router.push('/');
}
}
});
var Addpost = Vue.extend({
template: '#add-post',
data: function () {
return {post: {content: '', user_id: 1, category_id: 1}}
},
methods: {
createpost: function() {
var post = this.post;
api.post('/posts',post).then(function (response) {
post.id = response.data;
}).catch(function (error) {
console.log(error);
});
router.push('/');
}
}
});
var router = new VueRouter({routes:[
{ path: '/', component: List},
{ path: '/post/:post_id', component: post, name: 'post'},
{ path: '/add-post', component: Addpost},
{ path: '/post/:post_id/edit', component: postEdit, name: 'post-edit'},
{ path: '/post/:post_id/delete', component: postDelete, name: 'post-delete'}
]});
app = new Vue({
router:router
}).$mount('#app')
</script>
</body>
</html>

View File

@@ -0,0 +1,76 @@
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zepto/1.1.6/zepto.min.js"></script>
<script id="PostListTemplate" type="text/html">
<ul>
<li>
<span class="id"></span>, <span class="content"></span>
<a href="javascript:void(0)" class="edit">edit</a>
<a href="javascript:void(0)" class="delete">del</a>
</li>
<li>
<form>
<input name="content"/>
</form>
</li>
</ul>
</script>
<script>
function PostList(element,template) {
var self = this;
var url = '/api.php/records/posts';
self.edit = function() {
var li = $(this).parent('li');
var id = li.find('span.id').text();
var content = li.find('span.content').text();
content = prompt('Value', content);
if (content!==null) {
$.ajax({url: url + '/' + id, type: 'PUT', data: {content: content}, success: self.update});
}
};
self.delete = function() {
var li = $(this).parent('li');
var id = li.find('span.id').text();
if (confirm("Deleting #" + id + ". Continue?")) {
$.ajax({url: url + '/' + id, type: 'DELETE', success: self.update});
}
};
self.submit = function(e) {
e.preventDefault();
var content = $(this).find('input[name="content"]').val();
$.post(url, {user_id: 1, category_id: 1, content: content}, self.update);
};
self.render = function(data) {
data = data;
element.html(template.html());
var item = element.find('li').first().remove();
for (var i=0;i<data.records.length; i++) {
var clone = item.clone();
clone.find('span').each(function(){
var field = $(this).attr("class");
$(this).text(data.records[i][field]);
});
clone.insertBefore(element.find('li').last());
}
};
self.update = function() {
$.get(url, self.render);
};
self.post = function() {
$.post(url, {user_id: 1, category_id: 1, content: "from zepto"}, self.update);
};
element.on('submit', 'form', self.submit);
element.on('click', 'a.edit', self.edit);
element.on('click', 'a.delete', self.delete);
self.post();
};
$(function(){
new PostList($('#PostListDiv'), $('#PostListTemplate'));
});
</script>
</head>
<body>
<div id="PostListDiv">Loading...</div>
</body>
</html>

View File

@@ -0,0 +1,22 @@
<html>
<head>
<title>PHP-CRUD-API examples</title>
</head>
<body>
<h1>Example clients</h1>
<ul>
<li><a href="/examples/clients/auth.php/vanilla.html">Auth.php + VanillaJS</a></li>
<li><a href="/examples/clients/auth0/vanilla.html">Auth0 + VanillaJS</a></li>
<li><a href="/examples/clients/upload/vanilla.html">Upload + VanillaJS</a></li>
<li><a href="/examples/clients/vanilla.html">VanillaJS</a></li>
<li><a href="/examples/clients/angular.html">Angular</a></li>
<li><a href="/examples/clients/angular2.html">Angular 2</a></li>
<li><a href="/examples/clients/handlebars.html">Handlebars</a></li>
<li><a href="/examples/clients/knockout.html">Knockout</a></li>
<li><a href="/examples/clients/mustache.html">Mustache</a></li>
<li><a href="/examples/clients/react.html">React</a></li>
<li><a href="/examples/clients/vue.html">Vue</a></li>
<li><a href="/examples/clients/zepto.html">Zepto</a></li>
</ul>
</body>
</html>

View File

@@ -0,0 +1,66 @@
<?php
// get the HTTP method, path and body of the request
$method = $_SERVER['REQUEST_METHOD'];
$request = explode('/', trim($_SERVER['PATH_INFO'],'/'));
$input = json_decode(file_get_contents('php://input'),true);
if (!$input) $input = array();
// connect to the mysql database
$link = mysqli_connect('localhost', 'php-crud-api', 'php-crud-api', 'php-crud-api');
mysqli_set_charset($link,'utf8');
// retrieve the table and key from the path
$table = preg_replace('/[^a-z0-9_]+/i','',array_shift($request));
$key = array_shift($request)+0;
// escape the columns and values from the input object
$columns = preg_replace('/[^a-z0-9_]+/i','',array_keys($input));
$values = array_map(function ($value) use ($link) {
if ($value===null) return null;
return mysqli_real_escape_string($link,(string)$value);
},array_values($input));
// build the SET part of the SQL command
$set = '';
for ($i=0;$i<count($columns);$i++) {
$set.=($i>0?',':'').'`'.$columns[$i].'`=';
$set.=($values[$i]===null?'NULL':'"'.$values[$i].'"');
}
// create SQL based on HTTP method
switch ($method) {
case 'GET':
$sql = "select * from `$table`".($key?" WHERE id=$key":''); break;
case 'PUT':
$sql = "update `$table` set $set where id=$key"; break;
case 'POST':
$sql = "insert into `$table` set $set"; break;
case 'DELETE':
$sql = "delete from `$table` where id=$key"; break;
}
// execute SQL statement
$result = mysqli_query($link,$sql);
// die if SQL statement failed
if (!$result) {
http_response_code(404);
die(mysqli_error($link));
}
// print results, insert id or affected row count
if ($method == 'GET') {
if (!$key) echo '[';
for ($i=0;$i<mysqli_num_rows($result);$i++) {
echo ($i>0?',':'').json_encode(mysqli_fetch_object($result));
}
if (!$key) echo ']';
} elseif ($method == 'POST') {
echo mysqli_insert_id($link);
} else {
echo mysqli_affected_rows($link);
}
// close mysql connection
mysqli_close($link);

View File

@@ -0,0 +1,11 @@
<?php
// download composer and install dependencies
if (!file_exists('composer.phar')) {
$composer = file_get_contents('https://getcomposer.org/composer.phar');
file_put_contents('composer.phar', $composer);
}
exec('php composer.phar install --ignore-platform-reqs');
include 'patch.php';

51
seatmap-webapi/patch.php Normal file
View File

@@ -0,0 +1,51 @@
<?php
// patch files for PHP 7.0 compatibility
function patchDir(string $base, string $dir): int
{
$count = 0;
$entries = scandir($dir);
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
$filename = "$base/$dir/$entry";
if (is_dir($filename)) {
$count += patchDir($base, "$dir/$entry");
}
}
foreach ($entries as $entry) {
$filename = "$base/$dir/$entry";
if (is_file($filename)) {
if (substr($entry, -4) != '.php') {
continue;
}
$patched = $original = file_get_contents($filename);
$patched = preg_replace('/\):\s*(\?[a-zA-Z]+|void)\s*\n/', ") /*:$1*/\n", $patched);
$patched = preg_replace('/([\(,])\s*(\?[a-zA-Z]+|void)\s+\$/', "$1 /*$2*/ \$", $patched);
$patched = preg_replace('/(private|public|protected) const/', "/*$1*/ const", $patched);
if ($patched && $patched != $original) {
file_put_contents($filename, $patched);
$count++;
}
}
}
return $count;
}
function patch(string $base, array $dirs)
{
$start = microtime(true);
$count = 0;
foreach ($dirs as $dir) {
$count += patchDir($base, $dir);
}
$end = microtime(true);
$time = ($end - $start) * 1000;
if ($count) {
fwrite(STDERR, sprintf("%d files patched in %d ms\n", $count, $time));
}
}
patch(__DIR__, ['vendor']);

View File

@@ -0,0 +1,215 @@
<?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));
}
}

View File

@@ -0,0 +1,10 @@
<?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;
}

View File

@@ -0,0 +1,27 @@
<?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;
}
}

View File

@@ -0,0 +1,45 @@
<?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();
}
}

View File

@@ -0,0 +1,16 @@
<?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);
}
}

View File

@@ -0,0 +1,25 @@
<?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;
}
}

View File

@@ -0,0 +1,38 @@
<?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();
}
}

View File

@@ -0,0 +1,149 @@
<?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;
}
}

View File

@@ -0,0 +1,159 @@
<?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;
}
}

View File

@@ -0,0 +1,213 @@
<?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());
}
}

View File

@@ -0,0 +1,71 @@
<?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();
}
}

View File

@@ -0,0 +1,169 @@
<?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();
}
}

View File

@@ -0,0 +1,103 @@
<?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);
}
}

View File

@@ -0,0 +1,205 @@
<?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('/(?<!^)[A-Z]/', '_$0', str_replace('.', '_', $key)));
$newValues[$key] = getenv($environmentKey, true) ?: $value;
}
return $newValues;
}
public function __construct(array $values)
{
$driver = $this->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);
}
}

View File

@@ -0,0 +1,26 @@
<?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());
}
}

View File

@@ -0,0 +1,162 @@
<?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);
}
}

View File

@@ -0,0 +1,61 @@
<?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);
}
}
}

View File

@@ -0,0 +1,24 @@
<?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);
}
}

View File

@@ -0,0 +1,26 @@
<?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());
}
}

View File

@@ -0,0 +1,176 @@
<?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));
}
}
}

View File

@@ -0,0 +1,12 @@
<?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;
}

View File

@@ -0,0 +1,73 @@
<?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;
}
}

View File

@@ -0,0 +1,119 @@
<?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);
}
}

View File

@@ -0,0 +1,217 @@
<?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);
}
}

View File

@@ -0,0 +1,104 @@
<?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);
}
}
}
}
}

View File

@@ -0,0 +1,340 @@
<?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
]));
}
}

View File

@@ -0,0 +1,446 @@
<?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);
}
}

View File

@@ -0,0 +1,206 @@
<?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();
}
}

View File

@@ -0,0 +1,124 @@
<?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());
}
}

View File

@@ -0,0 +1,219 @@
<?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;
}
}

View File

@@ -0,0 +1,32 @@
<?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();
}
}

View File

@@ -0,0 +1,32 @@
<?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;
});
}
}

View File

@@ -0,0 +1,127 @@
<?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);
}
}

View File

@@ -0,0 +1,64 @@
<?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();
}
}

View File

@@ -0,0 +1,28 @@
<?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);
}
}

View File

@@ -0,0 +1,103 @@
<?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);
}
}

View File

@@ -0,0 +1,46 @@
<?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;
}
}

View File

@@ -0,0 +1,118 @@
<?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);
}
}

View File

@@ -0,0 +1,21 @@
<?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;
}
}

View File

@@ -0,0 +1,94 @@
<?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;
}
}

View File

@@ -0,0 +1,42 @@
<?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;
}
}

View File

@@ -0,0 +1,152 @@
<?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);
}
}

View File

@@ -0,0 +1,57 @@
<?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;
}
}

View File

@@ -0,0 +1,68 @@
<?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);
}
}

View File

@@ -0,0 +1,56 @@
<?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);
}
}

View File

@@ -0,0 +1,156 @@
<?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);
}
}

View File

@@ -0,0 +1,98 @@
<?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);
}
}

View File

@@ -0,0 +1,51 @@
<?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);
}
}

View File

@@ -0,0 +1,103 @@
<?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);
}
}

Some files were not shown because too many files have changed in this diff Show More