This commit is contained in:
Maurits van der Schee 2018-08-06 07:49:08 +02:00
commit 7562ca591c
232 changed files with 11718 additions and 7733 deletions

11
.gitignore vendored
View file

@ -1,11 +0,0 @@
.buildpath
.project
.settings/
phpunit.phar
tests/Config.php
.idea/
vendor
composer.lock
tests/sqlite.db
.DS_Store
log.txt

View file

@ -1,24 +0,0 @@
dist: trusty
sudo: false
language: php
addons:
postgresql: "9.4"
php:
- 5.4
- 5.5
- 5.6
- 7.0
- 7.1
before_install:
- phpenv config-add travisphp.ini
- psql -c 'create database testing;' -U postgres
- mysql -e 'CREATE DATABASE testing;'
- cp tests/Config.php.travis tests/Config.php
script:
- curl https://phar.phpunit.de/phpunit-4.8.phar -L -o phpunit.phar && chmod +x phpunit.phar
- php phpunit.phar

21
CONTRIBUTING.md Normal file
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).

View file

@ -1,6 +1,6 @@
The MIT License (MIT)
MIT License
Copyright (c) 2016 Maurits van der Schee
Copyright (c) 2018 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

1446
README.md

File diff suppressed because it is too large Load diff

7223
api.php

File diff suppressed because it is too large Load diff

72
build.php Normal file
View file

@ -0,0 +1,72 @@
<?php
function runDir(String $base, String $dir, array &$lines): int
{
$count = 0;
$entries = scandir($dir);
sort($entries);
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
$filename = "$base/$dir/$entry";
if (is_dir($filename)) {
$count += runDir($base, "$dir/$entry", $lines);
}
}
foreach ($entries as $entry) {
$filename = "$base/$dir/$entry";
if (is_file($filename)) {
if (substr($entry, -4) != '.php') {
continue;
}
$data = file_get_contents($filename);
array_push($lines, "// file: $dir/$entry");
foreach (explode("\n", $data) as $line) {
if (!preg_match('/^<\?php|^namespace |^use |spl_autoload_register|^\s*\/\//', $line)) {
array_push($lines, $line);
}
}
$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
**/
namespace Tqdev\PhpCrudApi;
EOF;
foreach (explode("\n", $head) as $line) {
array_push($lines, $line);
}
}
function run(String $base, String $dir, String $filename)
{
$lines = [];
$start = microtime(true);
addHeader($lines);
$count = runDir($base, $dir, $lines);
$data = implode("\n", $lines);
$data = preg_replace('/\n\s*\n\s*\n/', "\n\n", $data);
file_put_contents('tmp_' . $filename, $data);
ob_start();
include 'tmp_' . $filename;
ob_end_clean();
rename('tmp_' . $filename, $filename);
$end = microtime(true);
$time = ($end - $start) * 1000;
echo sprintf("%d files combined in %d ms into '%s'\n", $count, $time, $filename);
}
run(__DIR__, 'src', 'api.php');

View file

@ -1,37 +1,40 @@
{
"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",
"sql-server",
"postgresql",
"php-api",
"postgis",
"crud",
"rest-api",
"openapi",
"swagger"
],
"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": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35"
},
"autoload": {
"files": ["api.php"]
}
"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"
},
"autoload": {
"psr-4": { "Tqdev\\PhpCrudApi\\": "src/Tqdev/PhpCrudApi" }
}
}

View file

@ -1,10 +0,0 @@
FROM centos:7
ADD packages.sh /usr/sbin/docker-packages
RUN docker-packages
ADD install.sh /usr/sbin/docker-install
RUN docker-install
ADD run.sh /usr/sbin/docker-run
CMD docker-run

View file

@ -1,24 +0,0 @@
#!/bin/bash
# install software
cd /root; git clone https://github.com/mevdschee/php-crud-api.git
# download phpunit 4.8 for PHP < 5.6
cd php-crud-api; wget https://phar.phpunit.de/phpunit-4.8.phar -O phpunit.phar
# copy dist config to config
cp tests/Config.php.dist tests/Config.php
# replace variables
sed -i 's/{{mysql_hostname}}/localhost/g' tests/Config.php
sed -i 's/{{mysql_username}}/php-crud-api/g' tests/Config.php
sed -i 's/{{mysql_password}}/php-crud-api/g' tests/Config.php
sed -i 's/{{mysql_database}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_hostname}}/localhost/g' tests/Config.php
sed -i 's/{{pgsql_username}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_password}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_database}}/php-crud-api/g' tests/Config.php
sed -i 's/{{sqlite_hostname}}//g' tests/Config.php
sed -i 's/{{sqlite_username}}//g' tests/Config.php
sed -i 's/{{sqlite_password}}//g' tests/Config.php
sed -i 's/{{sqlite_database}}/tests\/sqlite.db/g' tests/Config.php
# move comments
sed -i 's/\/\* Uncomment/\/\/ Uncomment/g' tests/Config.php
sed -i "s/'SQLServer'/\/\* 'SQLServer'/g" tests/Config.php

View file

@ -1,11 +0,0 @@
#!/bin/bash
# update should not be needed
# yum -y upgdate
# install: php / mysql / postgres / sqlite / tools
yum -y install \
php-cli php-xml \
mariadb-server mariadb php-mysql \
postgresql-server postgresql php-pgsql \
sqlite php-sqlite3 \
git wget

View file

@ -1,37 +0,0 @@
# initialize mysql
mysql_install_db > /dev/null
chown -R mysql:mysql /var/lib/mysql
# run mysql server
nohup /usr/libexec/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 utf8 COLLATE utf8_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
# initialize postgresql
su - -c "/usr/bin/initdb --auth-local peer --auth-host password -D /var/lib/pgsql/data" postgres > /dev/null
# run postgres server
nohup su - -c "/usr/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";
\q
EOF
# run the tests
cd /root/php-crud-api
git pull
php phpunit.phar

View file

@ -1,10 +0,0 @@
FROM debian:7
ADD packages.sh /usr/sbin/docker-packages
RUN docker-packages
ADD install.sh /usr/sbin/docker-install
RUN docker-install
ADD run.sh /usr/sbin/docker-run
CMD docker-run

View file

@ -1,24 +0,0 @@
#!/bin/bash
# install software
cd /root; git clone https://github.com/mevdschee/php-crud-api.git
# download phpunit 4.8 for PHP < 5.6
cd php-crud-api; wget https://phar.phpunit.de/phpunit-4.8.phar -O phpunit.phar
# copy dist config to config
cp tests/Config.php.dist tests/Config.php
# replace variables
sed -i 's/{{mysql_hostname}}/localhost/g' tests/Config.php
sed -i 's/{{mysql_username}}/php-crud-api/g' tests/Config.php
sed -i 's/{{mysql_password}}/php-crud-api/g' tests/Config.php
sed -i 's/{{mysql_database}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_hostname}}/localhost/g' tests/Config.php
sed -i 's/{{pgsql_username}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_password}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_database}}/php-crud-api/g' tests/Config.php
sed -i 's/{{sqlite_hostname}}//g' tests/Config.php
sed -i 's/{{sqlite_username}}//g' tests/Config.php
sed -i 's/{{sqlite_password}}//g' tests/Config.php
sed -i 's/{{sqlite_database}}/tests\/sqlite.db/g' tests/Config.php
# move comments
sed -i 's/\/\* Uncomment/\/\/ Uncomment/g' tests/Config.php
sed -i "s/'SQLServer'/\/\* 'SQLServer'/g" tests/Config.php

View file

@ -1,13 +0,0 @@
#!/bin/bash
# ensure noninteractive is enabled for apt
export DEBIAN_FRONTEND=noninteractive
# update (upgrade should not be needed)
apt-get -y update # && apt-get -y upgrade
# install: php / mysql / postgres / sqlite / tools
apt-get -y install \
php5-cli \
mysql-server mysql-client php5-mysql \
postgresql php5-pgsql \
sqlite php5-sqlite \
git wget

View file

@ -1,34 +0,0 @@
#!/bin/bash
# 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 utf8 COLLATE utf8_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
# run postgres server
nohup su - -c "/usr/lib/postgresql/9.1/bin/postgres -D /etc/postgresql/9.1/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";
\q
EOF
# run the tests
cd /root/php-crud-api
git pull
php phpunit.phar

View file

@ -1,10 +0,0 @@
FROM debian:8
ADD packages.sh /usr/sbin/docker-packages
RUN docker-packages
ADD install.sh /usr/sbin/docker-install
RUN docker-install
ADD run.sh /usr/sbin/docker-run
CMD docker-run

View file

@ -1,24 +0,0 @@
#!/bin/bash
# install software
cd /root; git clone https://github.com/mevdschee/php-crud-api.git
# download phpunit 4.8 for PHP < 5.6
cd php-crud-api; wget https://phar.phpunit.de/phpunit-4.8.phar -O phpunit.phar
# copy dist config to config
cp tests/Config.php.dist tests/Config.php
# replace variables
sed -i 's/{{mysql_hostname}}/localhost/g' tests/Config.php
sed -i 's/{{mysql_username}}/php-crud-api/g' tests/Config.php
sed -i 's/{{mysql_password}}/php-crud-api/g' tests/Config.php
sed -i 's/{{mysql_database}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_hostname}}/localhost/g' tests/Config.php
sed -i 's/{{pgsql_username}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_password}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_database}}/php-crud-api/g' tests/Config.php
sed -i 's/{{sqlite_hostname}}//g' tests/Config.php
sed -i 's/{{sqlite_username}}//g' tests/Config.php
sed -i 's/{{sqlite_password}}//g' tests/Config.php
sed -i 's/{{sqlite_database}}/tests\/sqlite.db/g' tests/Config.php
# move comments
sed -i 's/\/\* Uncomment/\/\/ Uncomment/g' tests/Config.php
sed -i "s/'SQLServer'/\/\* 'SQLServer'/g" tests/Config.php

View file

@ -1,13 +0,0 @@
#!/bin/bash
# ensure noninteractive is enabled for apt
export DEBIAN_FRONTEND=noninteractive
# update (upgrade should not be needed)
apt-get -y update # && apt-get -y upgrade
# install: php / mysql / postgres / sqlite / tools
apt-get -y install \
php5-cli \
mariadb-server mariadb-client php5-mysql \
postgresql php5-pgsql \
sqlite php5-sqlite \
git wget

View file

@ -1,36 +0,0 @@
#!/bin/bash
# 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 utf8 COLLATE utf8_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
# run postgres server
nohup su - -c "/usr/lib/postgresql/9.4/bin/postgres -D /etc/postgresql/9.4/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";
\q
EOF
# run the tests
cd /root/php-crud-api
git pull
php phpunit.phar

View file

@ -1,10 +0,0 @@
FROM debian:8
ADD packages.sh /usr/sbin/docker-packages
RUN docker-packages
ADD install.sh /usr/sbin/docker-install
RUN docker-install
ADD run.sh /usr/sbin/docker-run
CMD docker-run

View file

@ -1,24 +0,0 @@
#!/bin/bash
# install software
cd /root; git clone https://github.com/mevdschee/php-crud-api.git
# download phpunit 4.8 for PHP < 5.6
cd php-crud-api; wget https://phar.phpunit.de/phpunit-4.8.phar -O phpunit.phar
# copy dist config to config
cp tests/Config.php.dist tests/Config.php
# replace variables
sed -i 's/{{mysql_hostname}}/localhost/g' tests/Config.php
sed -i 's/{{mysql_username}}/php-crud-api/g' tests/Config.php
sed -i 's/{{mysql_password}}/php-crud-api/g' tests/Config.php
sed -i 's/{{mysql_database}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_hostname}}/localhost/g' tests/Config.php
sed -i 's/{{pgsql_username}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_password}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_database}}/php-crud-api/g' tests/Config.php
sed -i 's/{{sqlite_hostname}}//g' tests/Config.php
sed -i 's/{{sqlite_username}}//g' tests/Config.php
sed -i 's/{{sqlite_password}}//g' tests/Config.php
sed -i 's/{{sqlite_database}}/tests\/sqlite.db/g' tests/Config.php
# move comments
sed -i 's/\/\* Uncomment/\/\/ Uncomment/g' tests/Config.php
sed -i "s/'SQLServer'/\/\* 'SQLServer'/g" tests/Config.php

View file

@ -1,13 +0,0 @@
#!/bin/bash
# ensure noninteractive is enabled for apt
export DEBIAN_FRONTEND=noninteractive
# update (upgrade should not be needed)
apt-get -y update # && apt-get -y upgrade
# install: php / mysql / postgres / sqlite / tools
apt-get -y install \
php5-cli \
mysql-server mysql-client php5-mysql \
postgresql php5-pgsql \
sqlite php5-sqlite \
git wget

View file

@ -1,34 +0,0 @@
#!/bin/bash
# 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 utf8 COLLATE utf8_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
# run postgres server
nohup su - -c "/usr/lib/postgresql/9.4/bin/postgres -D /etc/postgresql/9.4/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";
\q
EOF
# run the tests
cd /root/php-crud-api
git pull
php phpunit.phar

View file

@ -1,10 +1,16 @@
FROM debian:9
ADD packages.sh /usr/sbin/docker-packages
RUN docker-packages
ARG DEBIAN_FRONTEND=noninteractive
ADD install.sh /usr/sbin/docker-install
RUN docker-install
# install: php / mysql / postgres / sqlite / tools / mssql deps
RUN apt-get update && apt-get -y install \
php-cli php-xml \
mariadb-server mariadb-client php-mysql \
postgresql php-pgsql \
postgresql-9.6-postgis-2.3 \
sqlite php-sqlite3 \
git wget
# install run script
ADD run.sh /usr/sbin/docker-run
CMD docker-run
CMD docker-run

View file

@ -1,24 +0,0 @@
#!/bin/bash
# install software
cd /root; git clone https://github.com/mevdschee/php-crud-api.git
# download phpunit 4.8 for PHP < 5.6
cd php-crud-api; wget https://phar.phpunit.de/phpunit-4.8.phar -O phpunit.phar
# copy dist config to config
cp tests/Config.php.dist tests/Config.php
# replace variables
sed -i 's/{{mysql_hostname}}/localhost/g' tests/Config.php
sed -i 's/{{mysql_username}}/php-crud-api/g' tests/Config.php
sed -i 's/{{mysql_password}}/php-crud-api/g' tests/Config.php
sed -i 's/{{mysql_database}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_hostname}}/localhost/g' tests/Config.php
sed -i 's/{{pgsql_username}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_password}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_database}}/php-crud-api/g' tests/Config.php
sed -i 's/{{sqlite_hostname}}//g' tests/Config.php
sed -i 's/{{sqlite_username}}//g' tests/Config.php
sed -i 's/{{sqlite_password}}//g' tests/Config.php
sed -i 's/{{sqlite_database}}/tests\/sqlite.db/g' tests/Config.php
# move comments
sed -i 's/\/\* Uncomment/\/\/ Uncomment/g' tests/Config.php
sed -i "s/'SQLServer'/\/\* 'SQLServer'/g" tests/Config.php

View file

@ -1,13 +0,0 @@
#!/bin/bash
# ensure noninteractive is enabled for apt
export DEBIAN_FRONTEND=noninteractive
# update (upgrade should not be needed)
apt-get -y update # && apt-get -y upgrade
# install: php / mysql / postgres / sqlite / tools
apt-get -y install \
php-cli php-xml \
mariadb-server mariadb-client php-mysql \
postgresql php-pgsql \
sqlite php-sqlite3 \
git wget

View file

@ -1,5 +1,9 @@
#!/bin/bash
echo "================================================"
echo " Debian 9"
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
@ -15,7 +19,9 @@ 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
@ -27,10 +33,22 @@ 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
git clone --quiet https://github.com/mevdschee/php-crud-api2.git
echo "done"
echo "------------------------------------------------"
# run the tests
cd /root/php-crud-api
git pull
php phpunit.phar
cd php-crud-api2
php test.php

View file

@ -5,6 +5,7 @@ for f in $FILES
do
if [[ -d "$f" ]]
then
docker rm "php-crud-api_$f" > /dev/null 2>&1
docker run -ti --name "php-crud-api_$f" "php-crud-api:$f"
fi
done

View file

@ -1,10 +0,0 @@
FROM ubuntu:12.04
ADD packages.sh /usr/sbin/docker-packages
RUN docker-packages
ADD install.sh /usr/sbin/docker-install
RUN docker-install
ADD run.sh /usr/sbin/docker-run
CMD docker-run

View file

@ -1,24 +0,0 @@
#!/bin/bash
# install software
cd /root; git clone https://github.com/mevdschee/php-crud-api.git
# download phpunit 4.8 for PHP < 5.6
cd php-crud-api; wget https://phar.phpunit.de/phpunit-4.8.phar -O phpunit.phar
# copy dist config to config
cp tests/Config.php.dist tests/Config.php
# replace variables
sed -i 's/{{mysql_hostname}}/localhost/g' tests/Config.php
sed -i 's/{{mysql_username}}/php-crud-api/g' tests/Config.php
sed -i 's/{{mysql_password}}/php-crud-api/g' tests/Config.php
sed -i 's/{{mysql_database}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_hostname}}/localhost/g' tests/Config.php
sed -i 's/{{pgsql_username}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_password}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_database}}/php-crud-api/g' tests/Config.php
sed -i 's/{{sqlite_hostname}}//g' tests/Config.php
sed -i 's/{{sqlite_username}}//g' tests/Config.php
sed -i 's/{{sqlite_password}}//g' tests/Config.php
sed -i 's/{{sqlite_database}}/tests\/sqlite.db/g' tests/Config.php
# move comments
sed -i 's/\/\* Uncomment/\/\/ Uncomment/g' tests/Config.php
sed -i "s/'SQLServer'/\/\* 'SQLServer'/g" tests/Config.php

View file

@ -1,13 +0,0 @@
#!/bin/bash
# ensure noninteractive is enabled for apt
export DEBIAN_FRONTEND=noninteractive
# update (upgrade should not be needed)
apt-get -y update # && apt-get -y upgrade
# install: php / mysql / postgres / sqlite / tools
apt-get -y install \
php5-cli \
mysql-server mysql-client php5-mysql \
postgresql php5-pgsql \
sqlite php5-sqlite \
git wget

View file

@ -1,34 +0,0 @@
#!/bin/bash
# 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 utf8 COLLATE utf8_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
# run postgres server
nohup su - -c "/usr/lib/postgresql/9.1/bin/postgres -D /etc/postgresql/9.1/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";
\q
EOF
# run the tests
cd /root/php-crud-api
git pull
php phpunit.phar

View file

@ -1,10 +0,0 @@
FROM ubuntu:14.04
ADD packages.sh /usr/sbin/docker-packages
RUN docker-packages
ADD install.sh /usr/sbin/docker-install
RUN docker-install
ADD run.sh /usr/sbin/docker-run
CMD docker-run

View file

@ -1,24 +0,0 @@
#!/bin/bash
# install software
cd /root; git clone https://github.com/mevdschee/php-crud-api.git
# download phpunit 4.8 for PHP < 5.6
cd php-crud-api; wget https://phar.phpunit.de/phpunit-4.8.phar -O phpunit.phar
# copy dist config to config
cp tests/Config.php.dist tests/Config.php
# replace variables
sed -i 's/{{mysql_hostname}}/localhost/g' tests/Config.php
sed -i 's/{{mysql_username}}/php-crud-api/g' tests/Config.php
sed -i 's/{{mysql_password}}/php-crud-api/g' tests/Config.php
sed -i 's/{{mysql_database}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_hostname}}/localhost/g' tests/Config.php
sed -i 's/{{pgsql_username}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_password}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_database}}/php-crud-api/g' tests/Config.php
sed -i 's/{{sqlite_hostname}}//g' tests/Config.php
sed -i 's/{{sqlite_username}}//g' tests/Config.php
sed -i 's/{{sqlite_password}}//g' tests/Config.php
sed -i 's/{{sqlite_database}}/tests\/sqlite.db/g' tests/Config.php
# move comments
sed -i 's/\/\* Uncomment/\/\/ Uncomment/g' tests/Config.php
sed -i "s/'SQLServer'/\/\* 'SQLServer'/g" tests/Config.php

View file

@ -1,13 +0,0 @@
#!/bin/bash
# ensure noninteractive is enabled for apt
export DEBIAN_FRONTEND=noninteractive
# update (upgrade should not be needed)
apt-get -y update # && apt-get -y upgrade
# install: php / mysql / postgres / sqlite / tools
apt-get -y install \
php5-cli \
mysql-server mysql-client php5-mysql \
postgresql php5-pgsql \
sqlite php5-sqlite \
git wget

View file

@ -1,34 +0,0 @@
#!/bin/bash
# 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 utf8 COLLATE utf8_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
# run postgres server
nohup su - -c "/usr/lib/postgresql/9.3/bin/postgres -D /etc/postgresql/9.3/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";
\q
EOF
# run the tests
cd /root/php-crud-api
git pull
php phpunit.phar

View file

@ -1,10 +0,0 @@
FROM ubuntu:16.04
ADD packages.sh /usr/sbin/docker-packages
RUN docker-packages
ADD install.sh /usr/sbin/docker-install
RUN docker-install
ADD run.sh /usr/sbin/docker-run
CMD docker-run

View file

@ -1,24 +0,0 @@
#!/bin/bash
# install software
cd /root; git clone https://github.com/mevdschee/php-crud-api.git
# download phpunit 4.8 for PHP < 5.6
cd php-crud-api; wget https://phar.phpunit.de/phpunit-4.8.phar -O phpunit.phar
# copy dist config to config
cp tests/Config.php.dist tests/Config.php
# replace variables
sed -i 's/{{mysql_hostname}}/localhost/g' tests/Config.php
sed -i 's/{{mysql_username}}/php-crud-api/g' tests/Config.php
sed -i 's/{{mysql_password}}/php-crud-api/g' tests/Config.php
sed -i 's/{{mysql_database}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_hostname}}/localhost/g' tests/Config.php
sed -i 's/{{pgsql_username}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_password}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_database}}/php-crud-api/g' tests/Config.php
sed -i 's/{{sqlite_hostname}}//g' tests/Config.php
sed -i 's/{{sqlite_username}}//g' tests/Config.php
sed -i 's/{{sqlite_password}}//g' tests/Config.php
sed -i 's/{{sqlite_database}}/tests\/sqlite.db/g' tests/Config.php
# move comments
sed -i 's/\/\* Uncomment/\/\/ Uncomment/g' tests/Config.php
sed -i "s/'SQLServer'/\/\* 'SQLServer'/g" tests/Config.php

View file

@ -1,13 +0,0 @@
#!/bin/bash
# ensure noninteractive is enabled for apt
export DEBIAN_FRONTEND=noninteractive
# update (upgrade should not be needed)
apt-get -y update # && apt-get -y upgrade
# install: php / mysql / postgres / sqlite / tools
apt-get -y install \
php-cli php-xml \
mariadb-server mariadb-client php-mysql \
postgresql php-pgsql \
sqlite php-sqlite3 \
git wget

View file

@ -1,10 +1,38 @@
FROM ubuntu:16.04
ADD packages.sh /usr/sbin/docker-packages
RUN docker-packages
ARG DEBIAN_FRONTEND=noninteractive
ADD install.sh /usr/sbin/docker-install
RUN docker-install
# install: php / mysql / postgres / sqlite / tools / mssql deps
RUN apt-get update && apt-get -y install \
php-cli php-xml \
mariadb-server mariadb-client php-mysql \
postgresql php-pgsql \
postgresql-9.5-postgis-2.2 \
sqlite php-sqlite3 \
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
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
CMD docker-run

View file

@ -1,24 +0,0 @@
#!/bin/bash
# install software
cd /root; git clone https://github.com/mevdschee/php-crud-api.git
# download phpunit 4.8 for PHP < 5.6
cd php-crud-api; wget https://phar.phpunit.de/phpunit-4.8.phar -O phpunit.phar
# copy dist config to config
cp tests/Config.php.dist tests/Config.php
# replace variables
sed -i 's/{{mysql_hostname}}/localhost/g' tests/Config.php
sed -i 's/{{mysql_username}}/php-crud-api/g' tests/Config.php
sed -i 's/{{mysql_password}}/php-crud-api/g' tests/Config.php
sed -i 's/{{mysql_database}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_hostname}}/localhost/g' tests/Config.php
sed -i 's/{{pgsql_username}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_password}}/php-crud-api/g' tests/Config.php
sed -i 's/{{pgsql_database}}/php-crud-api/g' tests/Config.php
sed -i 's/{{sqlite_hostname}}//g' tests/Config.php
sed -i 's/{{sqlite_username}}//g' tests/Config.php
sed -i 's/{{sqlite_password}}//g' tests/Config.php
sed -i 's/{{sqlite_database}}/tests\/sqlite.db/g' tests/Config.php
# move comments
sed -i 's/\/\* Uncomment/\/\/ Uncomment/g' tests/Config.php
sed -i "s/'SQLServer'/\/\* 'SQLServer'/g" tests/Config.php

View file

@ -1,13 +0,0 @@
#!/bin/bash
# ensure noninteractive is enabled for apt
export DEBIAN_FRONTEND=noninteractive
# update (upgrade should not be needed)
apt-get -y update # && apt-get -y upgrade
# install: php / mysql / postgres / sqlite / tools
apt-get -y install \
php-cli php-xml \
mysql-server mysql-client php-mysql \
postgresql php-pgsql \
sqlite php-sqlite3 \
git wget

View file

@ -1,5 +1,9 @@
#!/bin/bash
echo "================================================"
echo " Ubuntu 16.04"
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
@ -15,7 +19,9 @@ 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
@ -27,10 +33,37 @@ 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/mysql.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
git clone --quiet https://github.com/mevdschee/php-crud-api2.git
echo "done"
echo "------------------------------------------------"
# run the tests
cd /root/php-crud-api
git pull
php phpunit.phar
cd php-crud-api2
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 \
mysql-server mysql-client php-mysql \
postgresql php-pgsql \
postgresql-10-postgis-2.4 \
sqlite 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

@ -1,5 +1,9 @@
#!/bin/bash
echo "================================================"
echo " Ubuntu 18.04"
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
@ -15,9 +19,13 @@ 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/9.5/bin/postgres -D /etc/postgresql/9.5/main" postgres > /root/postgres.log 2>&1 &
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;
@ -27,10 +35,22 @@ 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
git clone --quiet https://github.com/mevdschee/php-crud-api2.git
echo "done"
echo "------------------------------------------------"
# run the tests
cd /root/php-crud-api
git pull
php phpunit.phar
cd php-crud-api2
php test.php

View file

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

View file

@ -1,36 +0,0 @@
<?php
require "../lib/php_crud_api_transform.php";
function call($method, $url, $data = false) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_URL, $url);
if ($data) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
$headers = array();
$headers[] = 'Content-Type: application/json';
$headers[] = 'Content-Length: ' . strlen($data);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
}
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
return curl_exec($ch);
}
$response = call('GET', 'http://localhost/api.php/posts?include=categories');
$jsonObject = json_decode($response, true);
$jsonObject = php_crud_api_transform($jsonObject);
$output = json_encode($jsonObject, JSON_PRETTY_PRINT);
$object = array('user_id'=>1,'category_id'=>1,'content'=>'from php');
call('POST', 'http://localhost/api.php/posts',json_encode($object));
?>
<html>
<head>
</head>
<body>
<pre><?php echo $output ?></pre>
</body>
</html>

View file

@ -1,25 +0,0 @@
<html>
<head>
<script src= "http://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
<script src="../lib/php_crud_api_transform.js"></script>
<script>
var app = angular.module('myApplication', []);
app.controller('postController', function($scope, $http) {
var url = '../api.php/posts';
$http.post(url, {user_id: 1, category_id: 1, content: "from angular"}).success(function() {
$http.get(url).success(function(response) {
$scope.posts = php_crud_api_transform(response).posts;
});
});
});
</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

@ -1,32 +0,0 @@
<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 src="../lib/php_crud_api_transform.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/posts";
http.post(url,JSON.stringify({user_id:1,category_id:1,content:"from angular2"})).subscribe();
http.get(url).map(res => php_crud_api_transform(res.json())).subscribe(res => this.posts = res.posts);
}
]
});
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

@ -1,42 +0,0 @@
<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 src="../lib/php_crud_api_transform.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) {
// add withCredentials
let _build = http._backend._browserXHR.build;
http._backend._browserXHR.build = () => {
let _xhr = _build();
_xhr.withCredentials = true;
return _xhr;
};
var url = "../api.php";
http.post(url,JSON.stringify({username:"admin",password:"admin"})).subscribe(res => {
url += "/posts?csrf="+JSON.parse(res._body);
http.post(url,JSON.stringify({user_id:1,category_id:1,content:"from angular2"})).subscribe();
http.get(url).map(res => php_crud_api_transform(res.json())).subscribe(res => this.posts = res.posts);
});
}
]
});
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

@ -1,46 +0,0 @@
<?php
require "../lib/php_crud_api_transform.php";
$cookiejar = tempnam(sys_get_temp_dir(), 'cookiejar-');
function call($method, $url, $csrf = false, $data = false) {
global $cookiejar;
$ch = curl_init();
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_URL, $url);
$headers = array();
if ($data) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
$headers[] = 'Content-Type: application/json';
$headers[] = 'Content-Length: ' . strlen($data);
}
if ($csrf) {
$headers[] = 'X-XSRF-TOKEN: ' . $csrf;
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookiejar);
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookiejar);
return curl_exec($ch);
}
// in case you are using php-api-auth:
$csrf = json_decode(call('POST','http://localhost/api.php/', false, 'username=admin&password=admin'));
$response = call('GET','http://localhost/api.php/posts?include=categories,tags,comments&filter=id,eq,1', $csrf);
unlink($cookiejar);
$jsonObject = json_decode($response,true);
$jsonObject = php_crud_api_transform($jsonObject);
$output = json_encode($jsonObject,JSON_PRETTY_PRINT);
?>
<html>
<head>
</head>
<body>
<pre><?php echo $output ?></pre>
</body>
</html>

View file

@ -1,2 +0,0 @@
#!/bin/bash
curl 'http://localhost/api.php/posts?include=categories,tags,comments&filter=id,eq,1'

View file

@ -1,16 +0,0 @@
#!/bin/bash
# initialize cookie store
cp /dev/null cookies.txt
# login and store cookies in 'cookies.txt' AND retrieve the value of the XSRF token
TOKEN=`curl 'http://localhost/api.php/' --data "username=admin&password=admin" --cookie-jar cookies.txt --silent`
# strip the double quotes from the variable (JSON decode)
TOKEN=${TOKEN//\"/}
# set the XSRF token as the 'X-XSRF-Token' header AND send the cookies to the server
curl 'http://localhost/api.php/posts?include=categories,tags,comments&filter=id,eq,1' --header "X-XSRF-Token: $TOKEN" --cookie cookies.txt
# clean up
rm cookies.txt

View file

@ -1,67 +0,0 @@
<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 src="../lib/php_crud_api_transform.js"></script>
<script id="PostListTemplate" type="text/mustache">
<ul>
{{#posts}}
<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>
{{/posts}}
<li>
<form>
<input name="content"/>
</form>
</li>
</ul>
</script>
<script>
function PostList(element, template) {
var self = this;
var url = '../api.php/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())(php_crud_api_transform(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

@ -1,69 +0,0 @@
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.5/handlebars.runtime.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zepto/1.1.6/zepto.min.js"></script>
<script src="../lib/php_crud_api_transform.js"></script>
<script>
var PostListTemplate = {"1":function(container,depth0,helpers,partials,data) {
var helper;
return " <li>\n <span class=\"id\">"
+ container.escapeExpression(((helper = (helper = helpers.id || (depth0 != null ? depth0.id : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},{"name":"id","hash":{},"data":data}) : helper)))
+ "</span>, <span class=\"content\">"
+ container.escapeExpression(((helper = (helper = helpers.content || (depth0 != null ? depth0.content : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},{"name":"content","hash":{},"data":data}) : helper)))
+ "</span>\n <a href=\"javascript:void(0)\" class=\"edit\">edit</a>\n <a href=\"javascript:void(0)\" class=\"delete\">del</a>\n </li>\n";
},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
var stack1, helper, options, buffer =
"\n<ul>\n";
stack1 = ((helper = (helper = helpers.posts || (depth0 != null ? depth0.posts : depth0)) != null ? helper : helpers.helperMissing),(options={"name":"posts","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data}),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},options) : helper));
if (!helpers.posts) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}
if (stack1 != null) { buffer += stack1; }
return buffer + " <li>\n <form>\n <input name=\"content\"/>\n </form>\n </li>\n</ul>\n";
},"useData":true}
</script>
<script>
function PostList(element, template) {
var self = this;
var url = '../api.php/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.template(PostListTemplate)(php_crud_api_transform(data)));
};
self.update = function() {
$.get(url, self.render);
};
self.post = function() {
$.post(url, {user_id:1,category_id:1,content:"from handlebars_compiled"}, 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

@ -1,71 +0,0 @@
<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 src="../lib/php_crud_api_transform.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/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 = php_crud_api_transform(data).posts;
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

@ -1,67 +0,0 @@
<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 src="../lib/php_crud_api_transform.js"></script>
<script id="PostListTemplate" type="text/mustache">
<ul>
{{#posts}}
<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>
{{/posts}}
<li>
<form>
<input name="content"/>
</form>
</li>
</ul>
</script>
<script>
function PostList(element, template) {
var self = this;
var url = '../api.php/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(),php_crud_api_transform(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>

View file

@ -1,48 +0,0 @@
<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 src="../lib/php_crud_api_transform.js"></script>
<script>
var PostList = React.createClass({
displayName: 'PostList',
url: '../api.php/posts',
getInitialState: function() {
return { posts: [] };
},
retrieveServerState: function() {
this.serverRequest = $.get(this.url, function (data) {
this.setState(php_crud_api_transform(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.posts.map(createPost)
);
}
});
$(function(){ ReactDOM.render(React.createElement(PostList, null), document.getElementById('myApplication')); });
</script>
</head>
<body>
<div id="myApplication">Loading...</div>
</body>
</html>

View file

@ -1,251 +0,0 @@
<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>
<script src="../lib/php_crud_api_transform.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" requred/>
</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'
});
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 = php_crud_api_transform(response.data).posts;
}).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

@ -1,263 +0,0 @@
<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>
<script src="../lib/php_crud_api_transform.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" requred/>
</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',
withCredentials: true
});
api.interceptors.response.use(function (response) {
if (response.headers['x-xsrf-token']) {
document.cookie = 'XSRF-TOKEN=' + response.headers['x-xsrf-token'] + '; path=/';
}
return response;
});
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.post('/',{username:"admin",password:"admin"}).then(function (response) {
api.get('/posts').then(function (response) {
posts = self.posts = php_crud_api_transform(response.data).posts;
}).catch(function (error) {
console.log(error);
});
}).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

@ -1,77 +0,0 @@
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zepto/1.1.6/zepto.min.js"></script>
<script src="../lib/php_crud_api_transform.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/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 = php_crud_api_transform(data);
element.html(template.html());
var item = element.find('li').first().remove();
for (var i=0;i<data.posts.length; i++) {
var clone = item.clone();
clone.find('span').each(function(){
var field = $(this).attr("class");
$(this).text(data.posts[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

@ -1,66 +0,0 @@
<?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

@ -1,45 +0,0 @@
function php_crud_api_transform(tables) {
var array_flip = function (trans) {
var key, tmp_ar = {};
for (key in trans) {
tmp_ar[trans[key]] = key;
}
return tmp_ar;
};
var get_objects = function (tables,table_name,where_index,match_value) {
var objects = [];
for (var record in tables[table_name]['records']) {
record = tables[table_name]['records'][record];
if (!where_index || record[where_index]==match_value) {
var object = {};
for (var index in tables[table_name]['columns']) {
var column = tables[table_name]['columns'][index];
object[column] = record[index];
for (var relation in tables) {
var reltable = tables[relation];
for (var key in reltable['relations']) {
var target = reltable['relations'][key];
if (target == table_name+'.'+column) {
column_indices = array_flip(reltable['columns']);
object[relation] = get_objects(tables,relation,column_indices[key],record[index]);
}
}
}
}
objects.push(object);
}
}
return objects;
};
tree = {};
for (var name in tables) {
var table = tables[name];
if (!table['relations']) {
tree[name] = get_objects(tables,name);
if (table['results']) {
tree['_results'] = table['results'];
}
}
}
return tree;
}

View file

@ -1,38 +0,0 @@
<?php
function php_crud_api_transform(&$tables) {
$get_objects = function (&$tables,$table_name,$where_index=false,$match_value=false) use (&$get_objects) {
$objects = array();
if (isset($tables[$table_name]['records'])) {
foreach ($tables[$table_name]['records'] as $record) {
if ($where_index===false || $record[$where_index]==$match_value) {
$object = array();
foreach ($tables[$table_name]['columns'] as $index=>$column) {
$object[$column] = $record[$index];
foreach ($tables as $relation=>$reltable) {
if (isset($reltable['relations'])) {
foreach ($reltable['relations'] as $key=>$target) {
if ($target == "$table_name.$column") {
$column_indices = array_flip($reltable['columns']);
$object[$relation] = $get_objects($tables,$relation,$column_indices[$key],$record[$index]);
}
}
}
}
}
$objects[] = $object;
}
}
}
return $objects;
};
$tree = array();
foreach ($tables as $name=>$table) {
if (!isset($table['relations'])) {
$tree[$name] = $get_objects($tables,$name);
if (isset($table['results'])) {
$tree['_results'] = $table['results'];
}
}
}
return $tree;
}

View file

@ -1,35 +0,0 @@
def php_crud_api_transform(tables):
def get_objects(tables, table_name, where_index=None, match_value=None):
objects = []
for record in tables[table_name]['records']:
if where_index == None or (record[where_index] == match_value):
object = {}
columns = tables[table_name]['columns']
for column in columns:
index = columns.index(column)
object[column] = record[index]
for relation, reltable in tables.items():
for key, target in reltable.get('relations', {}).items():
if target == table_name + '.' + column:
relcols = reltable['columns']
column_indices = {value: relcols.index(value) for value in relcols}
object[relation] = get_objects(
tables, relation, column_indices[key], record[index])
objects.append(object)
return objects
tree = {}
for name, table in tables.items():
if not 'relations' in table:
tree[name] = get_objects(tables, name)
if 'results' in table:
tree['_results'] = table['results']
return tree
if __name__ == "__main__":
input = {"posts": {"columns": ["id","user_id","category_id","content"],"records": [[1,1,1,"blogstarted"]]},"post_tags": {"relations": {"post_id": "posts.id"},"columns": ["id","post_id","tag_id"],"records": [[1,1,1],[2,1,2]]},"categories": {"relations": {"id": "posts.category_id"},"columns": ["id","name"],"records": [[1,"anouncement"]]},"tags": {"relations": {"id": "post_tags.tag_id"},"columns": ["id","name"],"records": [[1,"funny"],[2,"important"]]},"comments": {"relations": {"post_id": "posts.id"},"columns": ["id","post_id","message"],"records": [[1,1,"great"],[2,1,"fantastic"]]}}
output = {"posts": [{"id": 1,"post_tags": [{"id": 1,"post_id": 1,"tag_id": 1,"tags": [{"id": 1,"name": "funny"}]},{"id": 2,"post_id": 1,"tag_id": 2,"tags": [{"id": 2,"name": "important"}]}],"comments": [{"id": 1,"post_id": 1,"message": "great"},{"id": 2,"post_id": 1,"message": "fantastic"}],"user_id": 1,"category_id": 1,"categories": [{"id": 1,"name": "anouncement"}],"content": "blogstarted"}]}
print(php_crud_api_transform(input) == output)

View file

@ -1,8 +0,0 @@
<phpunit bootstrap="tests/autoload.php">
<php>
<ini name="display_errors" value="true"/>
</php>
<testsuite name="All Tests">
<directory suffix='.php'>./tests/</directory>
</testsuite>
</phpunit>

View file

@ -0,0 +1,112 @@
<?php
namespace Tqdev\PhpCrudApi;
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\OpenApiController;
use Tqdev\PhpCrudApi\Controller\RecordController;
use Tqdev\PhpCrudApi\Controller\Responder;
use Tqdev\PhpCrudApi\Database\GenericDB;
use Tqdev\PhpCrudApi\Middleware\CorsMiddleware;
use Tqdev\PhpCrudApi\Middleware\FirewallMiddleware;
use Tqdev\PhpCrudApi\Middleware\Router\SimpleRouter;
use Tqdev\PhpCrudApi\Middleware\SanitationMiddleware;
use Tqdev\PhpCrudApi\Middleware\ValidationMiddleware;
use Tqdev\PhpCrudApi\OpenApi\OpenApiService;
use Tqdev\PhpCrudApi\Record\ErrorCode;
use Tqdev\PhpCrudApi\Record\RecordService;
class Api
{
private $router;
private $responder;
private $debug;
public function __construct(Config $config)
{
$db = new GenericDB(
$config->getDriver(),
$config->getAddress(),
$config->getPort(),
$config->getDatabase(),
$config->getUsername(),
$config->getPassword()
);
$cache = CacheFactory::create($config);
$reflection = new ReflectionService($db, $cache, $config->getCacheTime());
$definition = new DefinitionService($db, $reflection);
$responder = new Responder();
$router = new SimpleRouter($responder);
foreach ($config->getMiddlewares() as $middleware => $properties) {
switch ($middleware) {
case 'cors':
new CorsMiddleware($router, $responder, $properties);
break;
case 'firewall':
new FirewallMiddleware($router, $responder, $properties);
break;
case 'basicAuth':
new BasicAuthMiddleware($router, $responder, $properties);
break;
case 'validation':
new ValidationMiddleware($router, $responder, $properties, $reflection);
break;
case 'sanitation':
new SanitationMiddleware($router, $responder, $properties, $reflection);
break;
}
}
$data = new RecordService($db, $reflection);
$openApi = new OpenApiService($reflection);
foreach ($config->getControllers() as $controller) {
switch ($controller) {
case 'records':
new RecordController($router, $responder, $data);
break;
case 'columns':
new ColumnController($router, $responder, $reflection, $definition);
break;
case 'cache':
new CacheController($router, $responder, $cache);
break;
case 'openapi':
new OpenApiController($router, $responder, $openApi);
break;
}
}
$this->router = $router;
$this->responder = $responder;
$this->debug = $config->getDebug();
}
public function handle(Request $request): Response
{
$response = null;
try {
$response = $this->router->route($request);
} catch (\Throwable $e) {
if ($e instanceof \PDOException) {
if (strpos(strtolower($e->getMessage()), 'duplicate') !== false) {
return $this->responder->error(ErrorCode::DUPLICATE_KEY_EXCEPTION, '');
}
if (strpos(strtolower($e->getMessage()), 'default value') !== false) {
return $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
}
if (strpos(strtolower($e->getMessage()), 'allow nulls') !== false) {
return $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
}
if (strpos(strtolower($e->getMessage()), 'constraint') !== false) {
return $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
}
}
$response = $this->responder->error(ErrorCode::ERROR_NOT_FOUND, $e->getMessage());
if ($this->debug) {
$response->addHeader('X-Debug-Info', 'Exception in ' . $e->getFile() . ' on line ' . $e->getLine());
}
}
return $response;
}
}

View file

@ -0,0 +1,9 @@
<?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,35 @@
<?php
namespace Tqdev\PhpCrudApi\Cache;
use Tqdev\PhpCrudApi\Config;
class CacheFactory
{
const PREFIX = 'phpcrudapi-%s-';
private static function getPrefix(): String
{
return sprintf(self::PREFIX, substr(md5(__FILE__), 0, 8));
}
public static function create(Config $config): Cache
{
switch ($config->getCacheType()) {
case 'TempFile':
$cache = new TempFileCache(self::getPrefix(), $config->getCachePath());
break;
case 'Redis':
$cache = new RedisCache(self::getPrefix(), $config->getCachePath());
break;
case 'Memcache':
$cache = new MemcacheCache(self::getPrefix(), $config->getCachePath());
break;
case 'Memcached':
$cache = new MemcachedCache(self::getPrefix(), $config->getCachePath());
break;
default:
$cache = new NoCache();
}
return $cache;
}
}

View file

@ -0,0 +1,44 @@
<?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(): object
{
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,15 @@
<?php
namespace Tqdev\PhpCrudApi\Cache;
class MemcachedCache extends MemcacheCache
{
protected function create(): object
{
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,24 @@
<?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,37 @@
<?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,121 @@
<?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 == '') {
$id = substr(md5(__FILE__), 0, 8);
$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 file_put_contents($filename, $string, LOCK_EX) !== false;
}
private function getString($filename): String
{
$data = file_get_contents($filename);
if ($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 (is_file($filename)) {
if ($all || $this->getString($filename) == null) {
unlink($filename);
}
}
} else {
if (strlen($entry) != $segments[0]) {
continue;
}
if (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,158 @@
<?php
namespace Tqdev\PhpCrudApi\Column;
use Tqdev\PhpCrudApi\Database\GenericDB;
use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
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->get($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->get($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,165 @@
<?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();
}
public static function fromReflection(GenericReflection $reflection, array $columnResult): ReflectedColumn
{
$name = $columnResult['COLUMN_NAME'];
$length = $columnResult['CHARACTER_MAXIMUM_LENGTH'] + 0;
$type = $reflection->toJdbcType($columnResult['DATA_TYPE'], $length);
$precision = $columnResult['NUMERIC_PRECISION'] + 0;
$scale = $columnResult['NUMERIC_SCALE'] + 0;
$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) ? $json->length : 0;
$precision = isset($json->precision) ? $json->precision : 0;
$scale = isset($json->scale) ? $json->scale : 0;
$nullable = isset($json->nullable) ? $json->nullable : false;
$pk = isset($json->pk) ? $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 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,78 @@
<?php
namespace Tqdev\PhpCrudApi\Column\Reflection;
use Tqdev\PhpCrudApi\Database\GenericReflection;
class ReflectedDatabase implements \JsonSerializable
{
private $name;
private $tables;
public function __construct(String $name, array $tables)
{
$this->name = $name;
$this->tables = [];
foreach ($tables as $table) {
$this->tables[$table->getName()] = $table;
}
}
public static function fromReflection(GenericReflection $reflection): ReflectedDatabase
{
$name = $reflection->getDatabaseName();
$tables = [];
foreach ($reflection->getTables() as $tableName) {
if (in_array($tableName['TABLE_NAME'], $reflection->getIgnoredTables())) {
continue;
}
$table = ReflectedTable::fromReflection($reflection, $tableName);
$tables[$table->getName()] = $table;
}
return new ReflectedDatabase($name, array_values($tables));
}
public static function fromJson( /* object */$json): ReflectedDatabase
{
$name = $json->name;
$tables = [];
if (isset($json->tables) && is_array($json->tables)) {
foreach ($json->tables as $table) {
$tables[] = ReflectedTable::fromJson($table);
}
}
return new ReflectedDatabase($name, $tables);
}
public function getName(): String
{
return $this->name;
}
public function exists(String $tableName): bool
{
return isset($this->tables[$tableName]);
}
public function get(String $tableName): ReflectedTable
{
return $this->tables[$tableName];
}
public function getTableNames(): array
{
return array_keys($this->tables);
}
public function serialize()
{
return [
'name' => $this->name,
'tables' => array_values($this->tables),
];
}
public function jsonSerialize()
{
return $this->serialize();
}
}

View file

@ -0,0 +1,131 @@
<?php
namespace Tqdev\PhpCrudApi\Column\Reflection;
use Tqdev\PhpCrudApi\Database\GenericReflection;
class ReflectedTable implements \JsonSerializable
{
private $name;
private $columns;
private $pk;
private $fks;
public function __construct(String $name, array $columns)
{
$this->name = $name;
// 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, array $tableResult): ReflectedTable
{
$name = $tableResult['TABLE_NAME'];
// set columns
$columns = [];
foreach ($reflection->getTableColumns($name) as $tableColumn) {
$column = ReflectedColumn::fromReflection($reflection, $tableColumn);
$columns[$column->getName()] = $column;
}
// set primary key
$columnNames = $reflection->getTablePrimaryKeys($name);
if (count($columnNames) == 1) {
$columnName = $columnNames[0];
if (isset($columns[$columnName])) {
$pk = $columns[$columnName];
$pk->setPk(true);
}
}
// set foreign keys
$fks = $reflection->getTableForeignKeys($name);
foreach ($fks as $columnName => $table) {
$columns[$columnName]->setFk($table);
}
return new ReflectedTable($name, array_values($columns));
}
public static function fromJson( /* object */$json): ReflectedTable
{
$name = $json->name;
$columns = [];
if (isset($json->columns) && is_array($json->columns)) {
foreach ($json->columns as $column) {
$columns[] = ReflectedColumn::fromJson($column);
}
}
return new ReflectedTable($name, $columns);
}
public function exists(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 columnNames(): array
{
return array_keys($this->columns);
}
public function get($columnName): ReflectedColumn
{
return $this->columns[$columnName];
}
public function getFksTo(String $tableName): array
{
$columns = array();
foreach ($this->fks as $columnName => $referencedTableName) {
if ($tableName == $referencedTableName) {
$columns[] = $this->columns[$columnName];
}
}
return $columns;
}
public function serialize()
{
return [
'name' => $this->name,
'columns' => array_values($this->columns),
];
}
public function jsonSerialize()
{
return $this->serialize();
}
}

View file

@ -0,0 +1,56 @@
<?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 $tables;
public function __construct(GenericDB $db, Cache $cache, int $ttl)
{
$this->db = $db;
$this->cache = $cache;
$this->ttl = $ttl;
$this->tables = $this->loadTables(true);
}
private function loadTables(bool $useCache): ReflectedDatabase
{
$data = $useCache ? $this->cache->get('ReflectedDatabase') : '';
if ($data != '') {
$tables = ReflectedDatabase::fromJson(json_decode(gzuncompress($data)));
} else {
$tables = ReflectedDatabase::fromReflection($this->db->reflection());
$data = gzcompress(json_encode($tables, JSON_UNESCAPED_UNICODE));
$this->cache->set('ReflectedDatabase', $data, $this->ttl);
}
return $tables;
}
public function refresh()
{
$this->tables = $this->loadTables(false);
}
public function hasTable(String $table): bool
{
return $this->tables->exists($table);
}
public function getTable(String $table): ReflectedTable
{
return $this->tables->get($table);
}
public function getDatabase(): ReflectedDatabase
{
return $this->tables;
}
}

View file

@ -0,0 +1,153 @@
<?php
namespace Tqdev\PhpCrudApi;
class Config
{
private $values = [
'driver' => null,
'address' => 'localhost',
'port' => null,
'username' => null,
'password' => null,
'database' => null,
'middlewares' => 'cors',
'controllers' => 'records,columns,cache,openapi',
'cacheType' => 'TempFile',
'cachePath' => '',
'cacheTime' => 10,
'debug' => false,
];
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;
}
}
private function getDefaultAddress(String $driver): String
{
switch ($driver) {
case 'mysql':return 'localhost';
case 'pgsql':return 'localhost';
case 'sqlsrv':return 'localhost';
}
}
private function getDriverDefaults(String $driver): array
{
return [
'driver' => $driver,
'address' => $this->getDefaultAddress($driver),
'port' => $this->getDefaultPort($driver),
];
}
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'");
}
$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'] = $properties;
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 getMiddlewares(): array
{
return $this->values['middlewares'];
}
public function getControllers(): array
{
return array_map('trim', explode(',', $this->values['controllers']));
}
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(): String
{
return $this->values['debug'];
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Tqdev\PhpCrudApi\Controller;
use Tqdev\PhpCrudApi\Cache\Cache;
use Tqdev\PhpCrudApi\Request;
use Tqdev\PhpCrudApi\Response;
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(Request $request): Response
{
return $this->responder->success($this->cache->clear());
}
}

View file

@ -0,0 +1,156 @@
<?php
namespace Tqdev\PhpCrudApi\Controller;
use Tqdev\PhpCrudApi\Record\ErrorCode;
use Tqdev\PhpCrudApi\Column\DefinitionService;
use Tqdev\PhpCrudApi\Column\ReflectionService;
use Tqdev\PhpCrudApi\Request;
use Tqdev\PhpCrudApi\Response;
use Tqdev\PhpCrudApi\Middleware\Router\Router;
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(Request $request): Response
{
$database = $this->reflection->getDatabase();
return $this->responder->success($database);
}
public function getTable(Request $request): Response
{
$tableName = $request->getPathSegment(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(Request $request): Response
{
$tableName = $request->getPathSegment(2);
$columnName = $request->getPathSegment(3);
if (!$this->reflection->hasTable($tableName)) {
return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
}
$table = $this->reflection->getTable($tableName);
if (!$table->exists($columnName)) {
return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
}
$column = $table->get($columnName);
return $this->responder->success($column);
}
public function updateTable(Request $request): Response
{
$tableName = $request->getPathSegment(2);
if (!$this->reflection->hasTable($tableName)) {
return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
}
$success = $this->definition->updateTable($tableName, $request->getBody());
if ($success) {
$this->reflection->refresh();
}
return $this->responder->success($success);
}
public function updateColumn(Request $request): Response
{
$tableName = $request->getPathSegment(2);
$columnName = $request->getPathSegment(3);
if (!$this->reflection->hasTable($tableName)) {
return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
}
$table = $this->reflection->getTable($tableName);
if (!$table->exists($columnName)) {
return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
}
$success = $this->definition->updateColumn($tableName, $columnName, $request->getBody());
if ($success) {
$this->reflection->refresh();
}
return $this->responder->success($success);
}
public function addTable(Request $request): Response
{
$tableName = $request->getBody()->name;
if ($this->reflection->hasTable($tableName)) {
return $this->responder->error(ErrorCode::TABLE_ALREADY_EXISTS, $tableName);
}
$success = $this->definition->addTable($request->getBody());
if ($success) {
$this->reflection->refresh();
}
return $this->responder->success($success);
}
public function addColumn(Request $request): Response
{
$tableName = $request->getPathSegment(2);
if (!$this->reflection->hasTable($tableName)) {
return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
}
$columnName = $request->getBody()->name;
$table = $this->reflection->getTable($tableName);
if ($table->exists($columnName)) {
return $this->responder->error(ErrorCode::COLUMN_ALREADY_EXISTS, $columnName);
}
$success = $this->definition->addColumn($tableName, $request->getBody());
if ($success) {
$this->reflection->refresh();
}
return $this->responder->success($success);
}
public function removeTable(Request $request): Response
{
$tableName = $request->getPathSegment(2);
if (!$this->reflection->hasTable($tableName)) {
return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
}
$success = $this->definition->removeTable($tableName);
if ($success) {
$this->reflection->refresh();
}
return $this->responder->success($success);
}
public function removeColumn(Request $request): Response
{
$tableName = $request->getPathSegment(2);
$columnName = $request->getPathSegment(3);
if (!$this->reflection->hasTable($tableName)) {
return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
}
$table = $this->reflection->getTable($tableName);
if (!$table->exists($columnName)) {
return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
}
$success = $this->definition->removeColumn($tableName, $columnName);
if ($success) {
$this->reflection->refresh();
}
return $this->responder->success($success);
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Tqdev\PhpCrudApi\Controller;
use Tqdev\PhpCrudApi\OpenApi\OpenApiService;
use Tqdev\PhpCrudApi\Request;
use Tqdev\PhpCrudApi\Response;
use Tqdev\PhpCrudApi\Middleware\Router\Router;
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(Request $request): Response
{
return $this->responder->success(false);
}
}

View file

@ -0,0 +1,133 @@
<?php
namespace Tqdev\PhpCrudApi\Controller;
use Tqdev\PhpCrudApi\Record\ErrorCode;
use Tqdev\PhpCrudApi\Record\RecordService;
use Tqdev\PhpCrudApi\Request;
use Tqdev\PhpCrudApi\Response;
use Tqdev\PhpCrudApi\Middleware\Router\Router;
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(Request $request): Response
{
$table = $request->getPathSegment(2);
$params = $request->getParams();
if (!$this->service->exists($table)) {
return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
}
return $this->responder->success($this->service->_list($table, $params));
}
public function read(Request $request): Response
{
$table = $request->getPathSegment(2);
$id = $request->getPathSegment(3);
$params = $request->getParams();
if (!$this->service->exists($table)) {
return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
}
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(Request $request): Response
{
$table = $request->getPathSegment(2);
$record = $request->getBody();
if ($record === null) {
return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, '');
}
$params = $request->getParams();
if (!$this->service->exists($table)) {
return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
}
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(Request $request): Response
{
$table = $request->getPathSegment(2);
$id = $request->getPathSegment(3);
$record = $request->getBody();
if ($record === null) {
return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, '');
}
$params = $request->getParams();
if (!$this->service->exists($table)) {
return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
}
$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(Request $request): Response
{
$table = $request->getPathSegment(2);
$id = $request->getPathSegment(3);
$params = $request->getParams();
if (!$this->service->exists($table)) {
return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
}
$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));
}
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Tqdev\PhpCrudApi\Controller;
use Tqdev\PhpCrudApi\Record\Document\ErrorDocument;
use Tqdev\PhpCrudApi\Record\ErrorCode;
use Tqdev\PhpCrudApi\Response;
class Responder
{
public function error(int $error, String $argument, $details = null): Response
{
$errorCode = new ErrorCode($error);
$status = $errorCode->getStatus();
$document = new ErrorDocument($errorCode, $argument, $details);
return new Response($status, $document);
}
public function success($result): Response
{
return new Response(Response::OK, $result);
}
}

View file

@ -0,0 +1,64 @@
<?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->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 "CAST(N'' AS XML).value('xs:base64Binary(xs:hexBinary(sql:column($value)))', 'VARCHAR(MAX)') 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,106 @@
<?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";
}
}
private function quoteColumnName(ReflectedColumn $column): String
{
return '"' . $column->getName() . '"';
}
public function getOrderBy(ReflectedTable $table, array $columnOrdering): String
{
$results = array();
foreach ($columnOrdering as $i => list($columnName, $ordering)) {
$column = $table->get($columnName);
$quotedColumnName = $this->quoteColumnName($column);
$results[] = $quotedColumnName . ' ' . $ordering;
}
return implode(',', $results);
}
public function getSelect(ReflectedTable $table, array $columnNames): String
{
$results = array();
foreach ($columnNames as $columnName) {
$column = $table->get($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->get($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";
}
}
public function getUpdate(ReflectedTable $table, array $columnValues): String
{
$results = array();
foreach ($columnValues as $columnName => $columnValue) {
$column = $table->get($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->get($columnName);
$quotedColumnName = $this->quoteColumnName($column);
$columnValue = $this->converter->convertColumnValue($column);
$results[] = $quotedColumnName . '=' . $quotedColumnName . '+' . $columnValue;
}
return implode(',', $results);
}
}

View file

@ -0,0 +1,202 @@
<?php
namespace Tqdev\PhpCrudApi\Database;
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;
use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
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 ColumnCondition) {
return $this->getColumnConditionSql($condition, $arguments);
}
if ($condition instanceof SpatialCondition) {
return $this->getSpatialConditionSql($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($opertor, ['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 'sql_srv':
$functionName = str_replace('ST_', 'ST', $functionName);
$argument = $hasArgument ? 'geometry::STGeomFromText(?,0)' : '';
return "$column.$functionName($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,81 @@
<?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)
{
switch ($conversion) {
case 'boolean':
return $value ? true : false;
}
return $value;
}
private function getRecordValueConversion(ReflectedColumn $column): String
{
if (in_array($this->driver, ['mysql', 'sqlsrv']) && $column->isBoolean()) {
return 'boolean';
}
return 'none';
}
public function convertRecords(ReflectedTable $table, array $columnNames, array &$records) /*: void*/
{
foreach ($columnNames as $columnName) {
$column = $table->get($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 '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->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->get($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,228 @@
<?php
namespace Tqdev\PhpCrudApi\Database;
use Tqdev\PhpCrudApi\Record\Condition\ColumnCondition;
use Tqdev\PhpCrudApi\Record\Condition\Condition;
use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
class GenericDB
{
private $driver;
private $database;
private $pdo;
private $reflection;
private $columns;
private $conditions;
private $converter;
private function getDsn(String $address, String $port = null, String $database = null): String
{
switch ($this->driver) {
case 'mysql':return "$this->driver:host=$address;port=$port;dbname=$database;charset=utf8mb4";
case 'pgsql':return "$this->driver:host=$address port=$port dbname=$database options='--client_encoding=UTF8'";
case 'sqlsrv':return "$this->driver:Server=$address,$port;Database=$database";
}
}
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 [
];
}
}
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,
];
case 'pgsql':return $options + [
\PDO::ATTR_EMULATE_PREPARES => false,
];
case 'sqlsrv':return $options + [
\PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE => true,
];
}
}
public function __construct(String $driver, String $address, String $port = null, String $database = null, String $username = null, String $password = null)
{
$this->driver = $driver;
$this->database = $database;
$dsn = $this->getDsn($address, $port, $database);
$options = $this->getOptions();
$this->pdo = new \PDO($dsn, $username, $password, $options);
$commands = $this->getCommands();
foreach ($commands as $command) {
$this->pdo->query($command);
}
$this->reflection = new GenericReflection($this->pdo, $driver, $database);
$this->definition = new GenericDefinition($this->pdo, $driver, $database);
$this->conditions = new ConditionsBuilder($driver);
$this->columns = new ColumnsBuilder($driver);
$this->converter = new DataConverter($driver);
}
public function pdo(): \PDO
{
return $this->pdo;
}
public function reflection(): GenericReflection
{
return $this->reflection;
}
public function definition(): GenericDefinition
{
return $this->definition;
}
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;
}
return $stmt->fetchColumn(0);
}
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);
$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));
$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();
$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 selectAllUnordered(ReflectedTable $table, array $columnNames, Condition $condition): array
{
$selectColumns = $this->columns->getSelect($table, $columnNames);
$tableName = $table->getName();
$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 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();
$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 . ' ORDER BY ' . $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);
$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);
$parameters = array();
$whereClause = $this->conditions->getWhereClause($condition, $parameters);
$sql = 'DELETE FROM "' . $tableName . '" ' . $whereClause;
$stmt = $this->query($sql, $parameters);
return $stmt->rowCount();
}
private function query(String $sql, array $parameters): \PDOStatement
{
$stmt = $this->pdo->prepare($sql);
$stmt->execute($parameters);
//echo "- $sql -- " . json_encode($parameters, JSON_UNESCAPED_UNICODE) . "\n";
return $stmt;
}
}

View file

@ -0,0 +1,411 @@
<?php
namespace Tqdev\PhpCrudApi\Database;
use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
class GenericDefinition
{
private $pdo;
private $driver;
private $database;
private $typeConverter;
private $reflection;
public function __construct(\PDO $pdo, String $driver, String $database)
{
$this->pdo = $pdo;
$this->driver = $driver;
$this->database = $database;
$this->typeConverter = new TypeConverter($driver);
$this->reflection = new GenericReflection($pdo, $driver, $database);
}
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(), $column->getPk());
if ($column->hasPrecision() && $column->hasScale()) {
$size = '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
} else if ($column->hasPrecision()) {
$size = '(' . $column->getPrecision() . ')';
} else if ($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':
return '';
case 'sqlsrv':
return ($column->getPk() && !$update) ? ' IDENTITY(1,1)' : '';
}
}
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";
}
}
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'";
}
}
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 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);
$p3 = $this->pdo->quote($tableName . '_' . $columnName . '_seq');
switch ($this->driver) {
case 'mysql':
return "select 1";
case 'pgsql':
return "SELECT setval($p3, (SELECT max($p2)+1 FROM $p1));";
case 'sqlsrv':
return "ALTER SEQUENCE $p3 RESTART WITH (SELECT max($p2)+1 FROM $p1)";
}
}
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->pdo->quote($tableName . '_' . $columnName . '_seq');
$p4 = $this->quote('DF_' . $tableName . '_' . $columnName);
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->columnNames() as $columnName) {
$newColumn = $newTable->get($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()));
$fields[] = "$f1 $f2";
if ($newColumn->getPk()) {
$constraints[] = "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);
return "ALTER TABLE $p1 ADD COLUMN $p2 $p3";
}
private function getRemoveTableSQL(String $tableName): String
{
$p1 = $this->quote($tableName);
return "DROP TABLE $p1 CASCADE;";
}
private function getRemoveColumnSQL(String $tableName, String $columnName): String
{
$p1 = $this->quote($tableName);
$p2 = $this->quote($columnName);
return "ALTER TABLE $p1 DROP COLUMN $p2 CASCADE;";
}
public function renameTable(String $tableName, String $newTableName)
{
$sql = $this->getTableRenameSQL($tableName, $newTableName);
$stmt = $this->pdo->prepare($sql);
return $stmt->execute();
}
public function renameColumn(String $tableName, String $columnName, ReflectedColumn $newColumn)
{
$sql = $this->getColumnRenameSQL($tableName, $columnName, $newColumn);
$stmt = $this->pdo->prepare($sql);
return $stmt->execute();
}
public function retypeColumn(String $tableName, String $columnName, ReflectedColumn $newColumn)
{
$sql = $this->getColumnRetypeSQL($tableName, $columnName, $newColumn);
$stmt = $this->pdo->prepare($sql);
return $stmt->execute();
}
public function setColumnNullable(String $tableName, String $columnName, ReflectedColumn $newColumn)
{
$sql = $this->getSetColumnNullableSQL($tableName, $columnName, $newColumn);
$stmt = $this->pdo->prepare($sql);
return $stmt->execute();
}
public function addColumnPrimaryKey(String $tableName, String $columnName, ReflectedColumn $newColumn)
{
$sql = $this->getSetColumnPkConstraintSQL($tableName, $columnName, $newColumn);
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
if ($this->canAutoIncrement($newColumn)) {
$sql = $this->getSetColumnPkSequenceSQL($tableName, $columnName, $newColumn);
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
$sql = $this->getSetColumnPkSequenceStartSQL($tableName, $columnName, $newColumn);
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
$sql = $this->getSetColumnPkDefaultSQL($tableName, $columnName, $newColumn);
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
}
return true;
}
public function removeColumnPrimaryKey(String $tableName, String $columnName, ReflectedColumn $newColumn)
{
if ($this->canAutoIncrement($newColumn)) {
$sql = $this->getSetColumnPkDefaultSQL($tableName, $columnName, $newColumn);
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
$sql = $this->getSetColumnPkSequenceSQL($tableName, $columnName, $newColumn);
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
}
$sql = $this->getSetColumnPkConstraintSQL($tableName, $columnName, $newColumn);
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
return true;
}
public function addColumnForeignKey(String $tableName, String $columnName, ReflectedColumn $newColumn)
{
$sql = $this->getAddColumnFkConstraintSQL($tableName, $columnName, $newColumn);
$stmt = $this->pdo->prepare($sql);
return $stmt->execute();
}
public function removeColumnForeignKey(String $tableName, String $columnName, ReflectedColumn $newColumn)
{
$sql = $this->getRemoveColumnFkConstraintSQL($tableName, $columnName, $newColumn);
$stmt = $this->pdo->prepare($sql);
return $stmt->execute();
}
public function addTable(ReflectedTable $newTable)
{
$sql = $this->getAddTableSQL($newTable);
$stmt = $this->pdo->prepare($sql);
return $stmt->execute();
}
public function addColumn(String $tableName, ReflectedColumn $newColumn)
{
$sql = $this->getAddColumnSQL($tableName, $newColumn);
$stmt = $this->pdo->prepare($sql);
return $stmt->execute();
}
public function removeTable(String $tableName)
{
$sql = $this->getRemoveTableSQL($tableName);
$stmt = $this->pdo->prepare($sql);
return $stmt->execute();
}
public function removeColumn(String $tableName, String $columnName)
{
$sql = $this->getRemoveColumnSQL($tableName, $columnName);
$stmt = $this->pdo->prepare($sql);
return $stmt->execute();
}
}

View file

@ -0,0 +1,111 @@
<?php
namespace Tqdev\PhpCrudApi\Database;
class GenericReflection
{
private $pdo;
private $driver;
private $database;
private $typeConverter;
public function __construct(\PDO $pdo, String $driver, String $database)
{
$this->pdo = $pdo;
$this->driver = $driver;
$this->database = $database;
$this->typeConverter = new TypeConverter($driver);
}
public function getIgnoredTables(): array
{
switch ($this->driver) {
case 'mysql':return [];
case 'pgsql':return ['spatial_ref_sys'];
case 'sqlsrv':return [];
}
}
private function getTablesSQL(): String
{
switch ($this->driver) {
case 'mysql':return 'SELECT "TABLE_NAME" FROM "INFORMATION_SCHEMA"."TABLES" WHERE "TABLE_TYPE" IN (\'BASE TABLE\') AND "TABLE_SCHEMA" = ? ORDER BY BINARY "TABLE_NAME"';
case 'pgsql':return 'SELECT c.relname as "TABLE_NAME" FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind IN (\'r\') 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" FROM sysobjects o WHERE o.xtype = \'U\' ORDER BY "TABLE_NAME"';
}
}
private function getTableColumnsSQL(): String
{
switch ($this->driver) {
case 'mysql':return 'SELECT "COLUMN_NAME", "IS_NULLABLE", "DATA_TYPE", "CHARACTER_MAXIMUM_LENGTH", "NUMERIC_PRECISION", "NUMERIC_SCALE" FROM "INFORMATION_SCHEMA"."COLUMNS" WHERE "TABLE_NAME" = ? AND "TABLE_SCHEMA" = ?';
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" 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;';
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" 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 \'\' <> ?';
}
}
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 \'\' <> ?';
}
}
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 \'\' <> ?';
}
}
public function getDatabaseName(): String
{
return $this->database;
}
public function getTables(): array
{
$stmt = $this->pdo->prepare($this->getTablesSQL());
$stmt->execute([$this->database]);
return $stmt->fetchAll();
}
public function getTableColumns(String $tableName): array
{
$stmt = $this->pdo->prepare($this->getTableColumnsSQL());
$stmt->execute([$tableName, $this->database]);
return $stmt->fetchAll();
}
public function getTablePrimaryKeys(String $tableName): array
{
$stmt = $this->pdo->prepare($this->getTablePrimaryKeysSQL());
$stmt->execute([$tableName, $this->database]);
$results = $stmt->fetchAll();
$primaryKeys = [];
foreach ($results as $result) {
$primaryKeys[] = $result['COLUMN_NAME'];
}
return $primaryKeys;
}
public function getTableForeignKeys(String $tableName): array
{
$stmt = $this->pdo->prepare($this->getTableForeignKeysSQL());
$stmt->execute([$tableName, $this->database]);
$results = $stmt->fetchAll();
$foreignKeys = [];
foreach ($results as $result) {
$foreignKeys[$result['COLUMN_NAME']] = $result['REFERENCED_TABLE_NAME'];
}
return $foreignKeys;
}
public function toJdbcType(String $type, int $size): String
{
return $this->typeConverter->toJdbc($type, $size);
}
}

View file

@ -0,0 +1,187 @@
<?php
namespace Tqdev\PhpCrudApi\Database;
class TypeConverter
{
private $driver;
public function __construct(String $driver)
{
$this->driver = $driver;
}
private $fromJdbc = [
'mysql' => [
'clob' => 'longtext',
'boolean' => 'bit',
'blob' => 'longblob',
'timestamp' => 'datetime',
],
'pgsql' => [
'clob' => 'text',
'blob' => 'bytea',
],
'sqlsrv' => [
'boolean' => 'bit',
],
];
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',
'time_with_timezone' => 'time',
'timestamp_with_timezone' => 'timestamp',
],
'mysql' => [
'tinyint(1)' => 'boolean',
'bit(0)' => 'boolean',
'bit(1)' => 'boolean',
'tinyblob' => 'blob',
'mediumblob' => 'blob',
'longblob' => 'blob',
'tinytext' => 'clob',
'mediumtext' => 'clob',
'longtext' => 'clob',
'text' => 'clob',
'int' => 'integer',
'polygon' => 'geometry',
'point' => 'geometry',
'datetime' => 'timestamp',
],
'pgsql' => [
'bigserial' => 'bigint',
'bit varying' => 'bit',
'box' => 'geometry',
'bytea' => 'blob',
'character varying' => 'varchar',
'character' => 'char',
'cidr' => 'varchar',
'circle' => 'geometry',
'double precision' => 'double',
'inet' => 'integer',
//'interval [ fields ]'
'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(0)' => 'blob',
'bit' => 'boolean',
'datetime' => 'timestamp',
'datetime2' => 'timestamp',
'float' => 'double',
'image' => 'longvarbinary',
'int' => 'integer',
'money' => 'decimal',
'ntext' => 'longnvarchar',
'smalldatetime' => 'timestamp',
'smallmoney' => 'decimal',
'text' => 'longvarchar',
'timestamp' => 'binary',
'tinyint' => 'tinyint',
'udt' => 'varbinary',
'uniqueidentifier' => 'char',
'xml' => 'longnvarchar',
],
];
// 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, int $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'");
}
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,44 @@
<?php
namespace Tqdev\PhpCrudApi\Middleware;
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\Request;
use Tqdev\PhpCrudApi\Response;
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;
}
public function handle(Request $request): Response
{
$path = $request->getPathSegment(1);
$tableName = $request->getPathSegment(2);
$database = $this->reflection->getDatabase();
if ($path == 'records' && $database->exists($tableName)) {
$table = $database->get($tableName);
$method = $request->getMethod();
$tableHandler = $this->getProperty('tableHandler', '');
if ($tableHandler !== '') {
$valid = call_user_func($handler, $method, $tableName);
if ($valid !== true && $valid !== '') {
$details[$columnName] = $valid;
}
if (count($details) > 0) {
return $this->responder->error(ErrorCode::INPUT_VALIDATION_FAILED, $tableName, $details);
}
}
}
return $this->next->handle($request);
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace Tqdev\PhpCrudApi\Middleware\Base;
use Tqdev\PhpCrudApi\Request;
use Tqdev\PhpCrudApi\Response;
interface Handler
{
public function handle(Request $request): Response;
}

View file

@ -0,0 +1,29 @@
<?php
namespace Tqdev\PhpCrudApi\Middleware\Base;
use Tqdev\PhpCrudApi\Controller\Responder;
use Tqdev\PhpCrudApi\Middleware\Router\Router;
abstract class Middleware implements Handler
{
protected $next;
protected $responder;
private $properties;
public function __construct(Router $router, Responder $responder, array $properties)
{
$router->load($this);
$this->responder = $responder;
$this->properties = $properties;
}
public function setNext(Handler $handler) /*: void*/
{
$this->next = $handler;
}
protected function getProperty(String $key, $default)
{
return isset($this->properties[$key]) ? $this->properties[$key] : $default;
}
}

View file

@ -0,0 +1,86 @@
<?php
namespace Tqdev\PhpCrudApi\Middleware;
use Tqdev\PhpCrudApi\Controller\Responder;
use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
use Tqdev\PhpCrudApi\Record\ErrorCode;
use Tqdev\PhpCrudApi\Request;
use Tqdev\PhpCrudApi\Response;
class BasicAuthMiddleware extends Middleware
{
private function isAllowed(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 authenticate(String $username, String $password, String $passwordFile): bool
{
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (isset($_SESSION['user']) && $_SESSION['user'] == $username) {
return true;
}
$passwords = $this->readPasswords($passwordFile);
$allowed = $this->isAllowed($username, $password, $passwords);
if ($allowed) {
$_SESSION['user'] = $username;
}
$this->writePasswords($passwordFile, $passwords);
return $allowed;
}
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;
}
public function handle(Request $request): Response
{
$username = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : '';
$password = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : '';
$passwordFile = $this->getProperty('passwordFile', '.htpasswd');
if (!$username) {
$response = $this->responder->error(ErrorCode::AUTHORIZATION_REQUIRED, $username);
$realm = $this->getProperty('realm', 'Username and password required');
$response->addHeader('WWW-Authenticate', "Basic realm=\"$realm\"");
} elseif (!$this->authenticate($username, $password, $passwordFile)) {
$response = $this->responder->error(ErrorCode::ACCESS_DENIED, $username);
} else {
$response = $this->next->handle($request);
}
return $response;
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace Tqdev\PhpCrudApi\Middleware;
use Tqdev\PhpCrudApi\Controller\Responder;
use Tqdev\PhpCrudApi\Record\ErrorCode;
use Tqdev\PhpCrudApi\Request;
use Tqdev\PhpCrudApi\Response;
use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
class CorsMiddleware extends Middleware
{
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 handle(Request $request): Response
{
$method = $request->getMethod();
$origin = $request->getHeader('Origin');
$allowedOrigins = $this->getProperty('allowedOrigins', '*');
if ($origin && !$this->isOriginAllowed($origin, $allowedOrigins)) {
$response = $this->responder->error(ErrorCode::ORIGIN_FORBIDDEN, $origin);
} elseif ($method == 'OPTIONS') {
$response = new Response(Response::OK, '');
$allowHeaders = $this->getProperty('allowHeaders', 'Content-Type, X-XSRF-TOKEN');
$response->addHeader('Access-Control-Allow-Headers', $allowHeaders);
$allowMethods = $this->getProperty('allowMethods', 'OPTIONS, GET, PUT, POST, DELETE, PATCH');
$response->addHeader('Access-Control-Allow-Methods', $allowMethods);
$allowCredentials = $this->getProperty('allowCredentials', 'true');
$response->addHeader('Access-Control-Allow-Credentials', $allowCredentials);
$maxAge = $this->getProperty('maxAge', '1728000');
$response->addHeader('Access-Control-Max-Age', $maxAge);
} else {
$response = $this->next->handle($request);
}
if ($origin) {
$allowCredentials = $this->getProperty('allowCredentials', 'true');
$response->addHeader('Access-Control-Allow-Credentials', $allowCredentials);
$response->addHeader('Access-Control-Allow-Origin', $origin);
}
return $response;
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace Tqdev\PhpCrudApi\Middleware;
use Tqdev\PhpCrudApi\Controller\Responder;
use Tqdev\PhpCrudApi\Record\ErrorCode;
use Tqdev\PhpCrudApi\Request;
use Tqdev\PhpCrudApi\Response;
use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
class FirewallMiddleware extends Middleware
{
private function isIpAllowed(String $ipAddress, String $allowedIpAddresses): bool
{
foreach (explode(',', $allowedIpAddresses) as $allowedIp) {
if ($ipAddress == trim($allowedIp)) {
return true;
}
}
return false;
}
public function handle(Request $request): Response
{
$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::ACCESS_DENIED, $ipAddress);
} else {
$response = $this->next->handle($request);
}
return $response;
}
}

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