Maurits van der Schee 5 years ago
parent
commit
7562ca591c
100 changed files with 8398 additions and 5702 deletions
  1. 0
    11
      .gitignore
  2. 0
    24
      .travis.yml
  3. 21
    0
      CONTRIBUTING.md
  4. 2
    2
      LICENSE
  5. 488
    912
      README.md
  6. 4516
    2763
      api.php
  7. 72
    0
      build.php
  8. 38
    35
      composer.json
  9. 0
    10
      docker/centos7/Dockerfile
  10. 0
    24
      docker/centos7/install.sh
  11. 0
    11
      docker/centos7/packages.sh
  12. 0
    37
      docker/centos7/run.sh
  13. 0
    10
      docker/debian7/Dockerfile
  14. 0
    24
      docker/debian7/install.sh
  15. 0
    13
      docker/debian7/packages.sh
  16. 0
    34
      docker/debian7/run.sh
  17. 0
    10
      docker/debian8-mariadb/Dockerfile
  18. 0
    24
      docker/debian8-mariadb/install.sh
  19. 0
    13
      docker/debian8-mariadb/packages.sh
  20. 0
    36
      docker/debian8-mariadb/run.sh
  21. 0
    10
      docker/debian8/Dockerfile
  22. 0
    24
      docker/debian8/install.sh
  23. 0
    13
      docker/debian8/packages.sh
  24. 0
    34
      docker/debian8/run.sh
  25. 11
    5
      docker/debian9/Dockerfile
  26. 0
    24
      docker/debian9/install.sh
  27. 0
    13
      docker/debian9/packages.sh
  28. 21
    3
      docker/debian9/run.sh
  29. 1
    0
      docker/run_all.sh
  30. 0
    10
      docker/ubuntu12/Dockerfile
  31. 0
    24
      docker/ubuntu12/install.sh
  32. 0
    13
      docker/ubuntu12/packages.sh
  33. 0
    34
      docker/ubuntu12/run.sh
  34. 0
    10
      docker/ubuntu14/Dockerfile
  35. 0
    24
      docker/ubuntu14/install.sh
  36. 0
    13
      docker/ubuntu14/packages.sh
  37. 0
    34
      docker/ubuntu14/run.sh
  38. 0
    10
      docker/ubuntu16-mariadb/Dockerfile
  39. 0
    24
      docker/ubuntu16-mariadb/install.sh
  40. 0
    13
      docker/ubuntu16-mariadb/packages.sh
  41. 33
    5
      docker/ubuntu16/Dockerfile
  42. 0
    24
      docker/ubuntu16/install.sh
  43. 0
    13
      docker/ubuntu16/packages.sh
  44. 36
    3
      docker/ubuntu16/run.sh
  45. 21
    0
      docker/ubuntu18/Dockerfile
  46. 24
    4
      docker/ubuntu18/run.sh
  47. 0
    21
      examples/client.html
  48. 0
    36
      examples/client.php
  49. 0
    25
      examples/client_angular.html
  50. 0
    32
      examples/client_angular2.html
  51. 0
    42
      examples/client_angular2_auth.html
  52. 0
    46
      examples/client_auth.php
  53. 0
    2
      examples/client_bash.sh
  54. 0
    16
      examples/client_bash_auth.sh
  55. 0
    67
      examples/client_handlebars.html
  56. 0
    69
      examples/client_handlebars_compiled.html
  57. 0
    71
      examples/client_knockout.html
  58. 0
    67
      examples/client_mustache.html
  59. 0
    48
      examples/client_react.html
  60. 0
    251
      examples/client_vue.html
  61. 0
    263
      examples/client_vue_auth.html
  62. 0
    77
      examples/client_zepto.html
  63. 0
    66
      extras/core.php
  64. 0
    45
      lib/php_crud_api_transform.js
  65. 0
    38
      lib/php_crud_api_transform.php
  66. 0
    35
      lib/php_crud_api_transform.py
  67. 0
    8
      phpunit.xml
  68. 112
    0
      src/Tqdev/PhpCrudApi/Api.php
  69. 9
    0
      src/Tqdev/PhpCrudApi/Cache/Cache.php
  70. 35
    0
      src/Tqdev/PhpCrudApi/Cache/CacheFactory.php
  71. 44
    0
      src/Tqdev/PhpCrudApi/Cache/MemcacheCache.php
  72. 15
    0
      src/Tqdev/PhpCrudApi/Cache/MemcachedCache.php
  73. 24
    0
      src/Tqdev/PhpCrudApi/Cache/NoCache.php
  74. 37
    0
      src/Tqdev/PhpCrudApi/Cache/RedisCache.php
  75. 121
    0
      src/Tqdev/PhpCrudApi/Cache/TempFileCache.php
  76. 158
    0
      src/Tqdev/PhpCrudApi/Column/DefinitionService.php
  77. 165
    0
      src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedColumn.php
  78. 78
    0
      src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedDatabase.php
  79. 131
    0
      src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedTable.php
  80. 56
    0
      src/Tqdev/PhpCrudApi/Column/ReflectionService.php
  81. 153
    0
      src/Tqdev/PhpCrudApi/Config.php
  82. 26
    0
      src/Tqdev/PhpCrudApi/Controller/CacheController.php
  83. 156
    0
      src/Tqdev/PhpCrudApi/Controller/ColumnController.php
  84. 26
    0
      src/Tqdev/PhpCrudApi/Controller/OpenApiController.php
  85. 133
    0
      src/Tqdev/PhpCrudApi/Controller/RecordController.php
  86. 23
    0
      src/Tqdev/PhpCrudApi/Controller/Responder.php
  87. 64
    0
      src/Tqdev/PhpCrudApi/Database/ColumnConverter.php
  88. 106
    0
      src/Tqdev/PhpCrudApi/Database/ColumnsBuilder.php
  89. 202
    0
      src/Tqdev/PhpCrudApi/Database/ConditionsBuilder.php
  90. 81
    0
      src/Tqdev/PhpCrudApi/Database/DataConverter.php
  91. 228
    0
      src/Tqdev/PhpCrudApi/Database/GenericDB.php
  92. 411
    0
      src/Tqdev/PhpCrudApi/Database/GenericDefinition.php
  93. 111
    0
      src/Tqdev/PhpCrudApi/Database/GenericReflection.php
  94. 187
    0
      src/Tqdev/PhpCrudApi/Database/TypeConverter.php
  95. 44
    0
      src/Tqdev/PhpCrudApi/Middleware/AuthorizationMiddleware.php
  96. 10
    0
      src/Tqdev/PhpCrudApi/Middleware/Base/Handler.php
  97. 29
    0
      src/Tqdev/PhpCrudApi/Middleware/Base/Middleware.php
  98. 86
    0
      src/Tqdev/PhpCrudApi/Middleware/BasicAuthMiddleware.php
  99. 53
    0
      src/Tqdev/PhpCrudApi/Middleware/CorsMiddleware.php
  100. 0
    0
      src/Tqdev/PhpCrudApi/Middleware/FirewallMiddleware.php

+ 0
- 11
.gitignore View File

@@ -1,11 +0,0 @@
1
-.buildpath
2
-.project
3
-.settings/
4
-phpunit.phar
5
-tests/Config.php
6
-.idea/
7
-vendor
8
-composer.lock
9
-tests/sqlite.db
10
-.DS_Store
11
-log.txt

+ 0
- 24
.travis.yml View File

@@ -1,24 +0,0 @@
1
-dist: trusty
2
-sudo: false
3
-
4
-language: php
5
-
6
-addons:
7
-  postgresql: "9.4"
8
-
9
-php:
10
-  - 5.4
11
-  - 5.5
12
-  - 5.6
13
-  - 7.0
14
-  - 7.1
15
-
16
-before_install:
17
-  - phpenv config-add travisphp.ini
18
-  - psql -c 'create database testing;' -U postgres
19
-  - mysql -e 'CREATE DATABASE testing;'
20
-  - cp tests/Config.php.travis tests/Config.php
21
-
22
-script:
23
-  - curl https://phar.phpunit.de/phpunit-4.8.phar -L -o phpunit.phar && chmod +x phpunit.phar
24
-  - php phpunit.phar

+ 21
- 0
CONTRIBUTING.md View File

@@ -0,0 +1,21 @@
1
+# Contributing to php-crud-api
2
+
3
+Pull requests are welcome.
4
+
5
+## Use phpfmt
6
+
7
+Please use "phpfmt" to ensure consistent formatting.
8
+
9
+## Run the tests
10
+
11
+Before you do a PR, you should ensure any new functionality has test cases and that all existing tests are succeeding.
12
+
13
+## Run the build 
14
+
15
+Since this project is a single file application, you must ensure that classes are loaded in the correct order. 
16
+This is only important for the "extends" and "implements" relations. The 'build.php' script appends the classes in 
17
+alphabetical order (directories first). The path of the class that is extended or implemented (parent) must be above
18
+the extending or implementing (child) class when listing the contents of the 'src' directory in this order. If you
19
+get this order wrong you will see the build will fail with a "Class not found" error message. The solution is to
20
+rename the child class so that it starts with a later letter in the alphabet than the parent class or that you move
21
+the parent class to a subdirectory (directories are scanned first).

+ 2
- 2
LICENSE View File

@@ -1,6 +1,6 @@
1
-The MIT License (MIT)
1
+MIT License
2 2
 
3
-Copyright (c) 2016 Maurits van der Schee
3
+Copyright (c) 2018 Maurits van der Schee
4 4
 
5 5
 Permission is hereby granted, free of charge, to any person obtaining a copy
6 6
 of this software and associated documentation files (the "Software"), to deal

+ 488
- 912
README.md
File diff suppressed because it is too large
View File


+ 4516
- 2763
api.php
File diff suppressed because it is too large
View File


+ 72
- 0
build.php View File

@@ -0,0 +1,72 @@
1
+<?php
2
+
3
+function runDir(String $base, String $dir, array &$lines): int
4
+{
5
+    $count = 0;
6
+    $entries = scandir($dir);
7
+    sort($entries);
8
+    foreach ($entries as $entry) {
9
+        if ($entry === '.' || $entry === '..') {
10
+            continue;
11
+        }
12
+        $filename = "$base/$dir/$entry";
13
+        if (is_dir($filename)) {
14
+            $count += runDir($base, "$dir/$entry", $lines);
15
+        }
16
+    }
17
+    foreach ($entries as $entry) {
18
+        $filename = "$base/$dir/$entry";
19
+        if (is_file($filename)) {
20
+            if (substr($entry, -4) != '.php') {
21
+                continue;
22
+            }
23
+            $data = file_get_contents($filename);
24
+            array_push($lines, "// file: $dir/$entry");
25
+            foreach (explode("\n", $data) as $line) {
26
+                if (!preg_match('/^<\?php|^namespace |^use |spl_autoload_register|^\s*\/\//', $line)) {
27
+                    array_push($lines, $line);
28
+                }
29
+            }
30
+            $count++;
31
+        }
32
+    }
33
+    return $count;
34
+}
35
+
36
+function addHeader(array &$lines)
37
+{
38
+    $head = <<<EOF
39
+<?php
40
+/**
41
+ * PHP-CRUD-API v2              License: MIT
42
+ * Maurits van der Schee: maurits@vdschee.nl
43
+ * https://github.com/mevdschee/php-crud-api
44
+ **/
45
+
46
+namespace Tqdev\PhpCrudApi;
47
+
48
+EOF;
49
+    foreach (explode("\n", $head) as $line) {
50
+        array_push($lines, $line);
51
+    }
52
+}
53
+
54
+function run(String $base, String $dir, String $filename)
55
+{
56
+    $lines = [];
57
+    $start = microtime(true);
58
+    addHeader($lines);
59
+    $count = runDir($base, $dir, $lines);
60
+    $data = implode("\n", $lines);
61
+    $data = preg_replace('/\n\s*\n\s*\n/', "\n\n", $data);
62
+    file_put_contents('tmp_' . $filename, $data);
63
+    ob_start();
64
+    include 'tmp_' . $filename;
65
+    ob_end_clean();
66
+    rename('tmp_' . $filename, $filename);
67
+    $end = microtime(true);
68
+    $time = ($end - $start) * 1000;
69
+    echo sprintf("%d files combined in %d ms into '%s'\n", $count, $time, $filename);
70
+}
71
+
72
+run(__DIR__, 'src', 'api.php');

+ 38
- 35
composer.json View File

@@ -1,37 +1,40 @@
1 1
 {
2
-  "name": "mevdschee/php-crud-api",
3
-  "type": "library",
4
-  "description": "Single file PHP script that adds a REST API to a SQL database.",
5
-  "keywords": [
6
-    "api-server",
7
-    "restful",
8
-    "mysql",
9
-    "geospatial",
10
-    "sql-server",
11
-    "postgresql",
12
-    "php-api",
13
-    "postgis",
14
-    "crud",
15
-    "rest-api",
16
-    "openapi",
17
-    "swagger"
18
-  ],
19
-  "homepage": "https://github.com/mevdschee/php-crud-api",
20
-  "license": "MIT",
21
-  "authors": [
22
-    {
23
-      "name": "Maurits van der Schee",
24
-      "email": "maurits@vdschee.nl",
25
-      "homepage": "https://github.com/mevdschee"
26
-    }
27
-  ],
28
-  "require": {
29
-    "php": ">=5.3.0"
30
-  },
31
-  "require-dev": {
32
-    "phpunit/phpunit": "^4.8.35"
33
-  },
34
-  "autoload": {
35
-    "files": ["api.php"]
36
-  }
2
+    "name": "mevdschee/php-crud-api",
3
+    "type": "library",
4
+    "description": "Single file PHP script that adds a REST API to a SQL database.",
5
+    "keywords": [
6
+      "api-server",
7
+      "restful",
8
+      "mysql",
9
+      "geospatial",
10
+      "php",
11
+      "sql-server",
12
+      "postgresql",
13
+      "php-api",
14
+      "postgis",
15
+      "crud",
16
+      "rest-api",
17
+      "openapi",
18
+      "swagger",
19
+      "automatic-api",
20
+      "database",
21
+      "multi-database",
22
+      "sql-database",
23
+      "ubuntu-linux"
24
+    ],
25
+    "homepage": "https://github.com/mevdschee/php-crud-api",
26
+    "license": "MIT",
27
+    "authors": [
28
+      {
29
+        "name": "Maurits van der Schee",
30
+        "email": "maurits@vdschee.nl",
31
+        "homepage": "https://github.com/mevdschee"
32
+      }
33
+    ],
34
+    "require": {
35
+      "php": ">=7.0.0"
36
+    },
37
+    "autoload": {
38
+        "psr-4": { "Tqdev\\PhpCrudApi\\": "src/Tqdev/PhpCrudApi" }
39
+    }   
37 40
 }

+ 0
- 10
docker/centos7/Dockerfile View File

@@ -1,10 +0,0 @@
1
-FROM centos:7
2
-
3
-ADD packages.sh /usr/sbin/docker-packages
4
-RUN docker-packages
5
-
6
-ADD install.sh /usr/sbin/docker-install
7
-RUN docker-install
8
-
9
-ADD run.sh /usr/sbin/docker-run
10
-CMD docker-run

+ 0
- 24
docker/centos7/install.sh View File

@@ -1,24 +0,0 @@
1
-#!/bin/bash
2
-
3
-# install software
4
-cd /root; git clone https://github.com/mevdschee/php-crud-api.git
5
-# download phpunit 4.8 for PHP < 5.6
6
-cd php-crud-api; wget https://phar.phpunit.de/phpunit-4.8.phar -O phpunit.phar
7
-# copy dist config to config
8
-cp tests/Config.php.dist tests/Config.php
9
-# replace variables
10
-sed -i 's/{{mysql_hostname}}/localhost/g' tests/Config.php
11
-sed -i 's/{{mysql_username}}/php-crud-api/g' tests/Config.php
12
-sed -i 's/{{mysql_password}}/php-crud-api/g' tests/Config.php
13
-sed -i 's/{{mysql_database}}/php-crud-api/g' tests/Config.php
14
-sed -i 's/{{pgsql_hostname}}/localhost/g' tests/Config.php
15
-sed -i 's/{{pgsql_username}}/php-crud-api/g' tests/Config.php
16
-sed -i 's/{{pgsql_password}}/php-crud-api/g' tests/Config.php
17
-sed -i 's/{{pgsql_database}}/php-crud-api/g' tests/Config.php
18
-sed -i 's/{{sqlite_hostname}}//g' tests/Config.php
19
-sed -i 's/{{sqlite_username}}//g' tests/Config.php
20
-sed -i 's/{{sqlite_password}}//g' tests/Config.php
21
-sed -i 's/{{sqlite_database}}/tests\/sqlite.db/g' tests/Config.php
22
-# move comments
23
-sed -i 's/\/\* Uncomment/\/\/ Uncomment/g' tests/Config.php
24
-sed -i "s/'SQLServer'/\/\* 'SQLServer'/g" tests/Config.php

+ 0
- 11
docker/centos7/packages.sh View File

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

+ 0
- 37
docker/centos7/run.sh View File

@@ -1,37 +0,0 @@
1
-# initialize mysql
2
-mysql_install_db > /dev/null
3
-chown -R mysql:mysql /var/lib/mysql
4
-# run mysql server
5
-nohup /usr/libexec/mysqld -u mysql > /root/mysql.log 2>&1 &
6
-# wait for mysql to become available
7
-while ! mysqladmin ping -hlocalhost >/dev/null 2>&1; do
8
-    sleep 1
9
-done
10
-# create database and user on mysql
11
-mysql -u root >/dev/null << 'EOF'
12
-CREATE DATABASE `php-crud-api` CHARACTER SET utf8 COLLATE utf8_general_ci;
13
-CREATE USER 'php-crud-api'@'localhost' IDENTIFIED BY 'php-crud-api';
14
-GRANT ALL PRIVILEGES ON `php-crud-api`.* TO 'php-crud-api'@'localhost' WITH GRANT OPTION;
15
-FLUSH PRIVILEGES;
16
-EOF
17
-
18
-# initialize postgresql
19
-su - -c "/usr/bin/initdb --auth-local peer --auth-host password -D /var/lib/pgsql/data" postgres > /dev/null
20
-# run postgres server
21
-nohup su - -c "/usr/bin/postgres -D /var/lib/pgsql/data" postgres > /root/postgres.log 2>&1 &
22
-# wait for postgres to become available
23
-until su - -c "psql -U postgres -c '\q'" postgres >/dev/null 2>&1; do
24
-   sleep 1;
25
-done
26
-# create database and user on postgres
27
-su - -c "psql -U postgres >/dev/null" postgres << 'EOF'
28
-CREATE USER "php-crud-api" WITH PASSWORD 'php-crud-api';
29
-CREATE DATABASE "php-crud-api";
30
-GRANT ALL PRIVILEGES ON DATABASE "php-crud-api" to "php-crud-api";
31
-\q
32
-EOF
33
-
34
-# run the tests
35
-cd /root/php-crud-api
36
-git pull
37
-php phpunit.phar

+ 0
- 10
docker/debian7/Dockerfile View File

@@ -1,10 +0,0 @@
1
-FROM debian:7
2
-
3
-ADD packages.sh /usr/sbin/docker-packages
4
-RUN docker-packages
5
-
6
-ADD install.sh /usr/sbin/docker-install
7
-RUN docker-install
8
-
9
-ADD run.sh /usr/sbin/docker-run
10
-CMD docker-run

+ 0
- 24
docker/debian7/install.sh View File

@@ -1,24 +0,0 @@
1
-#!/bin/bash
2
-
3
-# install software
4
-cd /root; git clone https://github.com/mevdschee/php-crud-api.git
5
-# download phpunit 4.8 for PHP < 5.6
6
-cd php-crud-api; wget https://phar.phpunit.de/phpunit-4.8.phar -O phpunit.phar
7
-# copy dist config to config
8
-cp tests/Config.php.dist tests/Config.php
9
-# replace variables
10
-sed -i 's/{{mysql_hostname}}/localhost/g' tests/Config.php
11
-sed -i 's/{{mysql_username}}/php-crud-api/g' tests/Config.php
12
-sed -i 's/{{mysql_password}}/php-crud-api/g' tests/Config.php
13
-sed -i 's/{{mysql_database}}/php-crud-api/g' tests/Config.php
14
-sed -i 's/{{pgsql_hostname}}/localhost/g' tests/Config.php
15
-sed -i 's/{{pgsql_username}}/php-crud-api/g' tests/Config.php
16
-sed -i 's/{{pgsql_password}}/php-crud-api/g' tests/Config.php
17
-sed -i 's/{{pgsql_database}}/php-crud-api/g' tests/Config.php
18
-sed -i 's/{{sqlite_hostname}}//g' tests/Config.php
19
-sed -i 's/{{sqlite_username}}//g' tests/Config.php
20
-sed -i 's/{{sqlite_password}}//g' tests/Config.php
21
-sed -i 's/{{sqlite_database}}/tests\/sqlite.db/g' tests/Config.php
22
-# move comments
23
-sed -i 's/\/\* Uncomment/\/\/ Uncomment/g' tests/Config.php
24
-sed -i "s/'SQLServer'/\/\* 'SQLServer'/g" tests/Config.php

+ 0
- 13
docker/debian7/packages.sh View File

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

+ 0
- 34
docker/debian7/run.sh View File

@@ -1,34 +0,0 @@
1
-#!/bin/bash
2
-
3
-# run mysql server
4
-nohup mysqld > /root/mysql.log 2>&1 &
5
-# wait for mysql to become available
6
-while ! mysqladmin ping -hlocalhost >/dev/null 2>&1; do
7
-    sleep 1
8
-done
9
-# create database and user on mysql
10
-mysql -u root >/dev/null << 'EOF'
11
-CREATE DATABASE `php-crud-api` CHARACTER SET utf8 COLLATE utf8_general_ci;
12
-CREATE USER 'php-crud-api'@'localhost' IDENTIFIED BY 'php-crud-api';
13
-GRANT ALL PRIVILEGES ON `php-crud-api`.* TO 'php-crud-api'@'localhost' WITH GRANT OPTION;
14
-FLUSH PRIVILEGES;
15
-EOF
16
-
17
-# run postgres server
18
-nohup su - -c "/usr/lib/postgresql/9.1/bin/postgres -D /etc/postgresql/9.1/main" postgres > /root/postgres.log 2>&1 &
19
-# wait for postgres to become available
20
-until su - -c "psql -U postgres -c '\q'" postgres >/dev/null 2>&1; do
21
-   sleep 1;
22
-done
23
-# create database and user on postgres
24
-su - -c "psql -U postgres >/dev/null" postgres << 'EOF'
25
-CREATE USER "php-crud-api" WITH PASSWORD 'php-crud-api';
26
-CREATE DATABASE "php-crud-api";
27
-GRANT ALL PRIVILEGES ON DATABASE "php-crud-api" to "php-crud-api";
28
-\q
29
-EOF
30
-
31
-# run the tests
32
-cd /root/php-crud-api
33
-git pull
34
-php phpunit.phar

+ 0
- 10
docker/debian8-mariadb/Dockerfile View File

@@ -1,10 +0,0 @@
1
-FROM debian:8
2
-
3
-ADD packages.sh /usr/sbin/docker-packages
4
-RUN docker-packages
5
-
6
-ADD install.sh /usr/sbin/docker-install
7
-RUN docker-install
8
-
9
-ADD run.sh /usr/sbin/docker-run
10
-CMD docker-run

+ 0
- 24
docker/debian8-mariadb/install.sh View File

@@ -1,24 +0,0 @@
1
-#!/bin/bash
2
-
3
-# install software
4
-cd /root; git clone https://github.com/mevdschee/php-crud-api.git
5
-# download phpunit 4.8 for PHP < 5.6
6
-cd php-crud-api; wget https://phar.phpunit.de/phpunit-4.8.phar -O phpunit.phar
7
-# copy dist config to config
8
-cp tests/Config.php.dist tests/Config.php
9
-# replace variables
10
-sed -i 's/{{mysql_hostname}}/localhost/g' tests/Config.php
11
-sed -i 's/{{mysql_username}}/php-crud-api/g' tests/Config.php
12
-sed -i 's/{{mysql_password}}/php-crud-api/g' tests/Config.php
13
-sed -i 's/{{mysql_database}}/php-crud-api/g' tests/Config.php
14
-sed -i 's/{{pgsql_hostname}}/localhost/g' tests/Config.php
15
-sed -i 's/{{pgsql_username}}/php-crud-api/g' tests/Config.php
16
-sed -i 's/{{pgsql_password}}/php-crud-api/g' tests/Config.php
17
-sed -i 's/{{pgsql_database}}/php-crud-api/g' tests/Config.php
18
-sed -i 's/{{sqlite_hostname}}//g' tests/Config.php
19
-sed -i 's/{{sqlite_username}}//g' tests/Config.php
20
-sed -i 's/{{sqlite_password}}//g' tests/Config.php
21
-sed -i 's/{{sqlite_database}}/tests\/sqlite.db/g' tests/Config.php
22
-# move comments
23
-sed -i 's/\/\* Uncomment/\/\/ Uncomment/g' tests/Config.php
24
-sed -i "s/'SQLServer'/\/\* 'SQLServer'/g" tests/Config.php

+ 0
- 13
docker/debian8-mariadb/packages.sh View File

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

+ 0
- 36
docker/debian8-mariadb/run.sh View File

@@ -1,36 +0,0 @@
1
-#!/bin/bash
2
-
3
-# make sure mysql can create socket and lock
4
-mkdir /var/run/mysqld && chmod 777 /var/run/mysqld
5
-# run mysql server
6
-nohup mysqld > /root/mysql.log 2>&1 &
7
-# wait for mysql to become available
8
-while ! mysqladmin ping -hlocalhost >/dev/null 2>&1; do
9
-    sleep 1
10
-done
11
-# create database and user on mysql
12
-mysql -u root >/dev/null << 'EOF'
13
-CREATE DATABASE `php-crud-api` CHARACTER SET utf8 COLLATE utf8_general_ci;
14
-CREATE USER 'php-crud-api'@'localhost' IDENTIFIED BY 'php-crud-api';
15
-GRANT ALL PRIVILEGES ON `php-crud-api`.* TO 'php-crud-api'@'localhost' WITH GRANT OPTION;
16
-FLUSH PRIVILEGES;
17
-EOF
18
-
19
-# run postgres server
20
-nohup su - -c "/usr/lib/postgresql/9.4/bin/postgres -D /etc/postgresql/9.4/main" postgres > /root/postgres.log 2>&1 &
21
-# wait for postgres to become available
22
-until su - -c "psql -U postgres -c '\q'" postgres >/dev/null 2>&1; do
23
-   sleep 1;
24
-done
25
-# create database and user on postgres
26
-su - -c "psql -U postgres >/dev/null" postgres << 'EOF'
27
-CREATE USER "php-crud-api" WITH PASSWORD 'php-crud-api';
28
-CREATE DATABASE "php-crud-api";
29
-GRANT ALL PRIVILEGES ON DATABASE "php-crud-api" to "php-crud-api";
30
-\q
31
-EOF
32
-
33
-# run the tests
34
-cd /root/php-crud-api
35
-git pull
36
-php phpunit.phar

+ 0
- 10
docker/debian8/Dockerfile View File

@@ -1,10 +0,0 @@
1
-FROM debian:8
2
-
3
-ADD packages.sh /usr/sbin/docker-packages
4
-RUN docker-packages
5
-
6
-ADD install.sh /usr/sbin/docker-install
7
-RUN docker-install
8
-
9
-ADD run.sh /usr/sbin/docker-run
10
-CMD docker-run

+ 0
- 24
docker/debian8/install.sh View File

@@ -1,24 +0,0 @@
1
-#!/bin/bash
2
-
3
-# install software
4
-cd /root; git clone https://github.com/mevdschee/php-crud-api.git
5
-# download phpunit 4.8 for PHP < 5.6
6
-cd php-crud-api; wget https://phar.phpunit.de/phpunit-4.8.phar -O phpunit.phar
7
-# copy dist config to config
8
-cp tests/Config.php.dist tests/Config.php
9
-# replace variables
10
-sed -i 's/{{mysql_hostname}}/localhost/g' tests/Config.php
11
-sed -i 's/{{mysql_username}}/php-crud-api/g' tests/Config.php
12
-sed -i 's/{{mysql_password}}/php-crud-api/g' tests/Config.php
13
-sed -i 's/{{mysql_database}}/php-crud-api/g' tests/Config.php
14
-sed -i 's/{{pgsql_hostname}}/localhost/g' tests/Config.php
15
-sed -i 's/{{pgsql_username}}/php-crud-api/g' tests/Config.php
16
-sed -i 's/{{pgsql_password}}/php-crud-api/g' tests/Config.php
17
-sed -i 's/{{pgsql_database}}/php-crud-api/g' tests/Config.php
18
-sed -i 's/{{sqlite_hostname}}//g' tests/Config.php
19
-sed -i 's/{{sqlite_username}}//g' tests/Config.php
20
-sed -i 's/{{sqlite_password}}//g' tests/Config.php
21
-sed -i 's/{{sqlite_database}}/tests\/sqlite.db/g' tests/Config.php
22
-# move comments
23
-sed -i 's/\/\* Uncomment/\/\/ Uncomment/g' tests/Config.php
24
-sed -i "s/'SQLServer'/\/\* 'SQLServer'/g" tests/Config.php

+ 0
- 13
docker/debian8/packages.sh View File

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

+ 0
- 34
docker/debian8/run.sh View File

@@ -1,34 +0,0 @@
1
-#!/bin/bash
2
-
3
-# run mysql server
4
-nohup mysqld > /root/mysql.log 2>&1 &
5
-# wait for mysql to become available
6
-while ! mysqladmin ping -hlocalhost >/dev/null 2>&1; do
7
-    sleep 1
8
-done
9
-# create database and user on mysql
10
-mysql -u root >/dev/null << 'EOF'
11
-CREATE DATABASE `php-crud-api` CHARACTER SET utf8 COLLATE utf8_general_ci;
12
-CREATE USER 'php-crud-api'@'localhost' IDENTIFIED BY 'php-crud-api';
13
-GRANT ALL PRIVILEGES ON `php-crud-api`.* TO 'php-crud-api'@'localhost' WITH GRANT OPTION;
14
-FLUSH PRIVILEGES;
15
-EOF
16
-
17
-# run postgres server
18
-nohup su - -c "/usr/lib/postgresql/9.4/bin/postgres -D /etc/postgresql/9.4/main" postgres > /root/postgres.log 2>&1 &
19
-# wait for postgres to become available
20
-until su - -c "psql -U postgres -c '\q'" postgres >/dev/null 2>&1; do
21
-   sleep 1;
22
-done
23
-# create database and user on postgres
24
-su - -c "psql -U postgres >/dev/null" postgres << 'EOF'
25
-CREATE USER "php-crud-api" WITH PASSWORD 'php-crud-api';
26
-CREATE DATABASE "php-crud-api";
27
-GRANT ALL PRIVILEGES ON DATABASE "php-crud-api" to "php-crud-api";
28
-\q
29
-EOF
30
-
31
-# run the tests
32
-cd /root/php-crud-api
33
-git pull
34
-php phpunit.phar

+ 11
- 5
docker/debian9/Dockerfile View File

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

+ 0
- 24
docker/debian9/install.sh View File

@@ -1,24 +0,0 @@
1
-#!/bin/bash
2
-
3
-# install software
4
-cd /root; git clone https://github.com/mevdschee/php-crud-api.git
5
-# download phpunit 4.8 for PHP < 5.6
6
-cd php-crud-api; wget https://phar.phpunit.de/phpunit-4.8.phar -O phpunit.phar
7
-# copy dist config to config
8
-cp tests/Config.php.dist tests/Config.php
9
-# replace variables
10
-sed -i 's/{{mysql_hostname}}/localhost/g' tests/Config.php
11
-sed -i 's/{{mysql_username}}/php-crud-api/g' tests/Config.php
12
-sed -i 's/{{mysql_password}}/php-crud-api/g' tests/Config.php
13
-sed -i 's/{{mysql_database}}/php-crud-api/g' tests/Config.php
14
-sed -i 's/{{pgsql_hostname}}/localhost/g' tests/Config.php
15
-sed -i 's/{{pgsql_username}}/php-crud-api/g' tests/Config.php
16
-sed -i 's/{{pgsql_password}}/php-crud-api/g' tests/Config.php
17
-sed -i 's/{{pgsql_database}}/php-crud-api/g' tests/Config.php
18
-sed -i 's/{{sqlite_hostname}}//g' tests/Config.php
19
-sed -i 's/{{sqlite_username}}//g' tests/Config.php
20
-sed -i 's/{{sqlite_password}}//g' tests/Config.php
21
-sed -i 's/{{sqlite_database}}/tests\/sqlite.db/g' tests/Config.php
22
-# move comments
23
-sed -i 's/\/\* Uncomment/\/\/ Uncomment/g' tests/Config.php
24
-sed -i "s/'SQLServer'/\/\* 'SQLServer'/g" tests/Config.php

+ 0
- 13
docker/debian9/packages.sh View File

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

+ 21
- 3
docker/debian9/run.sh View File

@@ -1,5 +1,9 @@
1 1
 #!/bin/bash
2
+echo "================================================"
3
+echo " Debian 9"
4
+echo "================================================"
2 5
 
6
+echo -n "[1/4] Starting MariaDB 10.1 ..... "
3 7
 # make sure mysql can create socket and lock
4 8
 mkdir /var/run/mysqld && chmod 777 /var/run/mysqld
5 9
 # run mysql server
@@ -15,7 +19,9 @@ CREATE USER 'php-crud-api'@'localhost' IDENTIFIED BY 'php-crud-api';
15 19
 GRANT ALL PRIVILEGES ON `php-crud-api`.* TO 'php-crud-api'@'localhost' WITH GRANT OPTION;
16 20
 FLUSH PRIVILEGES;
17 21
 EOF
22
+echo "done"
18 23
 
24
+echo -n "[2/4] Starting PostgreSQL 9.6 ... "
19 25
 # run postgres server
20 26
 nohup su - -c "/usr/lib/postgresql/9.6/bin/postgres -D /etc/postgresql/9.6/main" postgres > /root/postgres.log 2>&1 &
21 27
 # wait for postgres to become available
@@ -27,10 +33,22 @@ su - -c "psql -U postgres >/dev/null" postgres << 'EOF'
27 33
 CREATE USER "php-crud-api" WITH PASSWORD 'php-crud-api';
28 34
 CREATE DATABASE "php-crud-api";
29 35
 GRANT ALL PRIVILEGES ON DATABASE "php-crud-api" to "php-crud-api";
36
+\c "php-crud-api";
37
+CREATE EXTENSION IF NOT EXISTS postgis;
30 38
 \q
31 39
 EOF
40
+echo "done"
41
+
42
+echo -n "[3/4] Starting SQLServer 2017 ... "
43
+echo "skipped"
44
+
45
+echo -n "[4/4] Cloning PHP-CRUD-API v2 ... "
46
+# install software
47
+git clone --quiet https://github.com/mevdschee/php-crud-api2.git
48
+echo "done"
49
+
50
+echo "------------------------------------------------"
32 51
 
33 52
 # run the tests
34
-cd /root/php-crud-api
35
-git pull
36
-php phpunit.phar
53
+cd php-crud-api2
54
+php test.php

+ 1
- 0
docker/run_all.sh View File

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

+ 0
- 10
docker/ubuntu12/Dockerfile View File

@@ -1,10 +0,0 @@
1
-FROM ubuntu:12.04
2
-
3
-ADD packages.sh /usr/sbin/docker-packages
4
-RUN docker-packages
5
-
6
-ADD install.sh /usr/sbin/docker-install
7
-RUN docker-install
8
-
9
-ADD run.sh /usr/sbin/docker-run
10
-CMD docker-run

+ 0
- 24
docker/ubuntu12/install.sh View File

@@ -1,24 +0,0 @@
1
-#!/bin/bash
2
-
3
-# install software
4
-cd /root; git clone https://github.com/mevdschee/php-crud-api.git
5
-# download phpunit 4.8 for PHP < 5.6
6
-cd php-crud-api; wget https://phar.phpunit.de/phpunit-4.8.phar -O phpunit.phar
7
-# copy dist config to config
8
-cp tests/Config.php.dist tests/Config.php
9
-# replace variables
10
-sed -i 's/{{mysql_hostname}}/localhost/g' tests/Config.php
11
-sed -i 's/{{mysql_username}}/php-crud-api/g' tests/Config.php
12
-sed -i 's/{{mysql_password}}/php-crud-api/g' tests/Config.php
13
-sed -i 's/{{mysql_database}}/php-crud-api/g' tests/Config.php
14
-sed -i 's/{{pgsql_hostname}}/localhost/g' tests/Config.php
15
-sed -i 's/{{pgsql_username}}/php-crud-api/g' tests/Config.php
16
-sed -i 's/{{pgsql_password}}/php-crud-api/g' tests/Config.php
17
-sed -i 's/{{pgsql_database}}/php-crud-api/g' tests/Config.php
18
-sed -i 's/{{sqlite_hostname}}//g' tests/Config.php
19
-sed -i 's/{{sqlite_username}}//g' tests/Config.php
20
-sed -i 's/{{sqlite_password}}//g' tests/Config.php
21
-sed -i 's/{{sqlite_database}}/tests\/sqlite.db/g' tests/Config.php
22
-# move comments
23
-sed -i 's/\/\* Uncomment/\/\/ Uncomment/g' tests/Config.php
24
-sed -i "s/'SQLServer'/\/\* 'SQLServer'/g" tests/Config.php

+ 0
- 13
docker/ubuntu12/packages.sh View File

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

+ 0
- 34
docker/ubuntu12/run.sh View File

@@ -1,34 +0,0 @@
1
-#!/bin/bash
2
-
3
-# run mysql server
4
-nohup mysqld > /root/mysql.log 2>&1 &
5
-# wait for mysql to become available
6
-while ! mysqladmin ping -hlocalhost >/dev/null 2>&1; do
7
-    sleep 1
8
-done
9
-# create database and user on mysql
10
-mysql -u root >/dev/null << 'EOF'
11
-CREATE DATABASE `php-crud-api` CHARACTER SET utf8 COLLATE utf8_general_ci;
12
-CREATE USER 'php-crud-api'@'localhost' IDENTIFIED BY 'php-crud-api';
13
-GRANT ALL PRIVILEGES ON `php-crud-api`.* TO 'php-crud-api'@'localhost' WITH GRANT OPTION;
14
-FLUSH PRIVILEGES;
15
-EOF
16
-
17
-# run postgres server
18
-nohup su - -c "/usr/lib/postgresql/9.1/bin/postgres -D /etc/postgresql/9.1/main" postgres > /root/postgres.log 2>&1 &
19
-# wait for postgres to become available
20
-until su - -c "psql -U postgres -c '\q'" postgres >/dev/null 2>&1; do
21
-   sleep 1;
22
-done
23
-# create database and user on postgres
24
-su - -c "psql -U postgres >/dev/null" postgres << 'EOF'
25
-CREATE USER "php-crud-api" WITH PASSWORD 'php-crud-api';
26
-CREATE DATABASE "php-crud-api";
27
-GRANT ALL PRIVILEGES ON DATABASE "php-crud-api" to "php-crud-api";
28
-\q
29
-EOF
30
-
31
-# run the tests
32
-cd /root/php-crud-api
33
-git pull
34
-php phpunit.phar

+ 0
- 10
docker/ubuntu14/Dockerfile View File

@@ -1,10 +0,0 @@
1
-FROM ubuntu:14.04
2
-
3
-ADD packages.sh /usr/sbin/docker-packages
4
-RUN docker-packages
5
-
6
-ADD install.sh /usr/sbin/docker-install
7
-RUN docker-install
8
-
9
-ADD run.sh /usr/sbin/docker-run
10
-CMD docker-run

+ 0
- 24
docker/ubuntu14/install.sh View File

@@ -1,24 +0,0 @@
1
-#!/bin/bash
2
-
3
-# install software
4
-cd /root; git clone https://github.com/mevdschee/php-crud-api.git
5
-# download phpunit 4.8 for PHP < 5.6
6
-cd php-crud-api; wget https://phar.phpunit.de/phpunit-4.8.phar -O phpunit.phar
7
-# copy dist config to config
8
-cp tests/Config.php.dist tests/Config.php
9
-# replace variables
10
-sed -i 's/{{mysql_hostname}}/localhost/g' tests/Config.php
11
-sed -i 's/{{mysql_username}}/php-crud-api/g' tests/Config.php
12
-sed -i 's/{{mysql_password}}/php-crud-api/g' tests/Config.php
13
-sed -i 's/{{mysql_database}}/php-crud-api/g' tests/Config.php
14
-sed -i 's/{{pgsql_hostname}}/localhost/g' tests/Config.php
15
-sed -i 's/{{pgsql_username}}/php-crud-api/g' tests/Config.php
16
-sed -i 's/{{pgsql_password}}/php-crud-api/g' tests/Config.php
17
-sed -i 's/{{pgsql_database}}/php-crud-api/g' tests/Config.php
18
-sed -i 's/{{sqlite_hostname}}//g' tests/Config.php
19
-sed -i 's/{{sqlite_username}}//g' tests/Config.php
20
-sed -i 's/{{sqlite_password}}//g' tests/Config.php
21
-sed -i 's/{{sqlite_database}}/tests\/sqlite.db/g' tests/Config.php
22
-# move comments
23
-sed -i 's/\/\* Uncomment/\/\/ Uncomment/g' tests/Config.php
24
-sed -i "s/'SQLServer'/\/\* 'SQLServer'/g" tests/Config.php

+ 0
- 13
docker/ubuntu14/packages.sh View File

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

+ 0
- 34
docker/ubuntu14/run.sh View File

@@ -1,34 +0,0 @@
1
-#!/bin/bash
2
-
3
-# run mysql server
4
-nohup mysqld > /root/mysql.log 2>&1 &
5
-# wait for mysql to become available
6
-while ! mysqladmin ping -hlocalhost >/dev/null 2>&1; do
7
-    sleep 1
8
-done
9
-# create database and user on mysql
10
-mysql -u root >/dev/null << 'EOF'
11
-CREATE DATABASE `php-crud-api` CHARACTER SET utf8 COLLATE utf8_general_ci;
12
-CREATE USER 'php-crud-api'@'localhost' IDENTIFIED BY 'php-crud-api';
13
-GRANT ALL PRIVILEGES ON `php-crud-api`.* TO 'php-crud-api'@'localhost' WITH GRANT OPTION;
14
-FLUSH PRIVILEGES;
15
-EOF
16
-
17
-# run postgres server
18
-nohup su - -c "/usr/lib/postgresql/9.3/bin/postgres -D /etc/postgresql/9.3/main" postgres > /root/postgres.log 2>&1 &
19
-# wait for postgres to become available
20
-until su - -c "psql -U postgres -c '\q'" postgres >/dev/null 2>&1; do
21
-   sleep 1;
22
-done
23
-# create database and user on postgres
24
-su - -c "psql -U postgres >/dev/null" postgres << 'EOF'
25
-CREATE USER "php-crud-api" WITH PASSWORD 'php-crud-api';
26
-CREATE DATABASE "php-crud-api";
27
-GRANT ALL PRIVILEGES ON DATABASE "php-crud-api" to "php-crud-api";
28
-\q
29
-EOF
30
-
31
-# run the tests
32
-cd /root/php-crud-api
33
-git pull
34
-php phpunit.phar

+ 0
- 10
docker/ubuntu16-mariadb/Dockerfile View File

@@ -1,10 +0,0 @@
1
-FROM ubuntu:16.04
2
-
3
-ADD packages.sh /usr/sbin/docker-packages
4
-RUN docker-packages
5
-
6
-ADD install.sh /usr/sbin/docker-install
7
-RUN docker-install
8
-
9
-ADD run.sh /usr/sbin/docker-run
10
-CMD docker-run

+ 0
- 24
docker/ubuntu16-mariadb/install.sh View File

@@ -1,24 +0,0 @@
1
-#!/bin/bash
2
-
3
-# install software
4
-cd /root; git clone https://github.com/mevdschee/php-crud-api.git
5
-# download phpunit 4.8 for PHP < 5.6
6
-cd php-crud-api; wget https://phar.phpunit.de/phpunit-4.8.phar -O phpunit.phar
7
-# copy dist config to config
8
-cp tests/Config.php.dist tests/Config.php
9
-# replace variables
10
-sed -i 's/{{mysql_hostname}}/localhost/g' tests/Config.php
11
-sed -i 's/{{mysql_username}}/php-crud-api/g' tests/Config.php
12
-sed -i 's/{{mysql_password}}/php-crud-api/g' tests/Config.php
13
-sed -i 's/{{mysql_database}}/php-crud-api/g' tests/Config.php
14
-sed -i 's/{{pgsql_hostname}}/localhost/g' tests/Config.php
15
-sed -i 's/{{pgsql_username}}/php-crud-api/g' tests/Config.php
16
-sed -i 's/{{pgsql_password}}/php-crud-api/g' tests/Config.php
17
-sed -i 's/{{pgsql_database}}/php-crud-api/g' tests/Config.php
18
-sed -i 's/{{sqlite_hostname}}//g' tests/Config.php
19
-sed -i 's/{{sqlite_username}}//g' tests/Config.php
20
-sed -i 's/{{sqlite_password}}//g' tests/Config.php
21
-sed -i 's/{{sqlite_database}}/tests\/sqlite.db/g' tests/Config.php
22
-# move comments
23
-sed -i 's/\/\* Uncomment/\/\/ Uncomment/g' tests/Config.php
24
-sed -i "s/'SQLServer'/\/\* 'SQLServer'/g" tests/Config.php

+ 0
- 13
docker/ubuntu16-mariadb/packages.sh View File

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

+ 33
- 5
docker/ubuntu16/Dockerfile View File

@@ -1,10 +1,38 @@
1 1
 FROM ubuntu:16.04
2 2
 
3
-ADD packages.sh /usr/sbin/docker-packages
4
-RUN docker-packages
3
+ARG DEBIAN_FRONTEND=noninteractive
5 4
 
6
-ADD install.sh /usr/sbin/docker-install
7
-RUN docker-install
5
+# install: php / mysql / postgres / sqlite / tools / mssql deps
6
+RUN apt-get update && apt-get -y install \
7
+php-cli php-xml \
8
+mariadb-server mariadb-client php-mysql \
9
+postgresql php-pgsql \
10
+postgresql-9.5-postgis-2.2 \
11
+sqlite php-sqlite3 \
12
+git wget \
13
+curl apt-transport-https debconf-utils sudo
8 14
 
15
+# adding custom MS repository
16
+RUN curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
17
+RUN curl https://packages.microsoft.com/config/ubuntu/16.04/prod.list > /etc/apt/sources.list.d/mssql-release.list
18
+RUN curl https://packages.microsoft.com/config/ubuntu/16.04/mssql-server-2017.list > /etc/apt/sources.list.d/mssql-server-2017.list
19
+
20
+# install SQL Server and tools
21
+RUN apt-get update && apt-get -y install mssql-server
22
+RUN ACCEPT_EULA=Y MSSQL_PID=Express MSSQL_SA_PASSWORD=sapwd123! /opt/mssql/bin/mssql-conf setup || true
23
+RUN ACCEPT_EULA=Y apt-get install -y msodbcsql mssql-tools
24
+
25
+# install pdo_sqlsrv
26
+RUN apt-get -y install php-pear build-essential unixodbc-dev php-dev
27
+RUN pecl install pdo_sqlsrv
28
+RUN echo extension=pdo_sqlsrv.so > /etc/php/7.0/mods-available/pdo_sqlsrv.ini
29
+RUN phpenmod pdo_sqlsrv
30
+
31
+# install locales
32
+RUN apt-get -y install locales
33
+RUN locale-gen en_US.UTF-8
34
+RUN update-locale LANG=en_US.UTF-8
35
+
36
+# install run script
9 37
 ADD run.sh /usr/sbin/docker-run
10
-CMD docker-run
38
+CMD docker-run

+ 0
- 24
docker/ubuntu16/install.sh View File

@@ -1,24 +0,0 @@
1
-#!/bin/bash
2
-
3
-# install software
4
-cd /root; git clone https://github.com/mevdschee/php-crud-api.git
5
-# download phpunit 4.8 for PHP < 5.6
6
-cd php-crud-api; wget https://phar.phpunit.de/phpunit-4.8.phar -O phpunit.phar
7
-# copy dist config to config
8
-cp tests/Config.php.dist tests/Config.php
9
-# replace variables
10
-sed -i 's/{{mysql_hostname}}/localhost/g' tests/Config.php
11
-sed -i 's/{{mysql_username}}/php-crud-api/g' tests/Config.php
12
-sed -i 's/{{mysql_password}}/php-crud-api/g' tests/Config.php
13
-sed -i 's/{{mysql_database}}/php-crud-api/g' tests/Config.php
14
-sed -i 's/{{pgsql_hostname}}/localhost/g' tests/Config.php
15
-sed -i 's/{{pgsql_username}}/php-crud-api/g' tests/Config.php
16
-sed -i 's/{{pgsql_password}}/php-crud-api/g' tests/Config.php
17
-sed -i 's/{{pgsql_database}}/php-crud-api/g' tests/Config.php
18
-sed -i 's/{{sqlite_hostname}}//g' tests/Config.php
19
-sed -i 's/{{sqlite_username}}//g' tests/Config.php
20
-sed -i 's/{{sqlite_password}}//g' tests/Config.php
21
-sed -i 's/{{sqlite_database}}/tests\/sqlite.db/g' tests/Config.php
22
-# move comments
23
-sed -i 's/\/\* Uncomment/\/\/ Uncomment/g' tests/Config.php
24
-sed -i "s/'SQLServer'/\/\* 'SQLServer'/g" tests/Config.php

+ 0
- 13
docker/ubuntu16/packages.sh View File

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

+ 36
- 3
docker/ubuntu16/run.sh View File

@@ -1,5 +1,9 @@
1 1
 #!/bin/bash
2
+echo "================================================"
3
+echo " Ubuntu 16.04"
4
+echo "================================================"
2 5
 
6
+echo -n "[1/4] Starting MariaDB 10.0 ..... "
3 7
 # make sure mysql can create socket and lock
4 8
 mkdir /var/run/mysqld && chmod 777 /var/run/mysqld
5 9
 # run mysql server
@@ -15,7 +19,9 @@ CREATE USER 'php-crud-api'@'localhost' IDENTIFIED BY 'php-crud-api';
15 19
 GRANT ALL PRIVILEGES ON `php-crud-api`.* TO 'php-crud-api'@'localhost' WITH GRANT OPTION;
16 20
 FLUSH PRIVILEGES;
17 21
 EOF
22
+echo "done"
18 23
 
24
+echo -n "[2/4] Starting PostgreSQL 9.5 ... "
19 25
 # run postgres server
20 26
 nohup su - -c "/usr/lib/postgresql/9.5/bin/postgres -D /etc/postgresql/9.5/main" postgres > /root/postgres.log 2>&1 &
21 27
 # wait for postgres to become available
@@ -27,10 +33,37 @@ su - -c "psql -U postgres >/dev/null" postgres << 'EOF'
27 33
 CREATE USER "php-crud-api" WITH PASSWORD 'php-crud-api';
28 34
 CREATE DATABASE "php-crud-api";
29 35
 GRANT ALL PRIVILEGES ON DATABASE "php-crud-api" to "php-crud-api";
36
+\c "php-crud-api";
37
+CREATE EXTENSION IF NOT EXISTS postgis;
30 38
 \q
31 39
 EOF
40
+echo "done"
41
+
42
+echo -n "[3/4] Starting SQLServer 2017 ... "
43
+# run sqlserver server
44
+nohup /opt/mssql/bin/sqlservr --accept-eula > /root/mysql.log 2>&1 &
45
+# create database and user on postgres
46
+/opt/mssql-tools/bin/sqlcmd -l 30 -S localhost -U SA -P sapwd123! >/dev/null << 'EOF'
47
+CREATE DATABASE [php-crud-api]
48
+GO
49
+CREATE LOGIN [php-crud-api] WITH PASSWORD=N'php-crud-api', DEFAULT_DATABASE=[php-crud-api], CHECK_EXPIRATION=OFF, CHECK_POLICY=OFF
50
+GO
51
+USE [php-crud-api]
52
+GO
53
+CREATE USER [php-crud-api] FOR LOGIN [php-crud-api] WITH DEFAULT_SCHEMA=[dbo]
54
+exec sp_addrolemember 'db_owner', 'php-crud-api';
55
+GO
56
+exit
57
+EOF
58
+echo "done"
59
+
60
+echo -n "[4/4] Cloning PHP-CRUD-API v2 ... "
61
+# install software
62
+git clone --quiet https://github.com/mevdschee/php-crud-api2.git
63
+echo "done"
64
+
65
+echo "------------------------------------------------"
32 66
 
33 67
 # run the tests
34
-cd /root/php-crud-api
35
-git pull
36
-php phpunit.phar
68
+cd php-crud-api2
69
+php test.php

+ 21
- 0
docker/ubuntu18/Dockerfile View File

@@ -0,0 +1,21 @@
1
+FROM ubuntu:18.04
2
+
3
+ARG DEBIAN_FRONTEND=noninteractive
4
+
5
+# install: php / mysql / postgres / sqlite / tools
6
+RUN apt-get update && apt-get -y install \
7
+php-cli php-xml \
8
+mysql-server mysql-client php-mysql \
9
+postgresql php-pgsql \
10
+postgresql-10-postgis-2.4 \
11
+sqlite php-sqlite3 \
12
+git wget
13
+
14
+# install locales
15
+RUN apt-get -y install locales
16
+RUN locale-gen en_US.UTF-8
17
+RUN update-locale LANG=en_US.UTF-8
18
+
19
+# install run script
20
+ADD run.sh /usr/sbin/docker-run
21
+CMD docker-run

docker/ubuntu16-mariadb/run.sh → docker/ubuntu18/run.sh View File

@@ -1,5 +1,9 @@
1 1
 #!/bin/bash
2
+echo "================================================"
3
+echo " Ubuntu 18.04"
4
+echo "================================================"
2 5
 
6
+echo -n "[1/4] Starting MySQL 5.7 ........ "
3 7
 # make sure mysql can create socket and lock
4 8
 mkdir /var/run/mysqld && chmod 777 /var/run/mysqld
5 9
 # run mysql server
@@ -15,9 +19,13 @@ CREATE USER 'php-crud-api'@'localhost' IDENTIFIED BY 'php-crud-api';
15 19
 GRANT ALL PRIVILEGES ON `php-crud-api`.* TO 'php-crud-api'@'localhost' WITH GRANT OPTION;
16 20
 FLUSH PRIVILEGES;
17 21
 EOF
22
+echo "done"
18 23
 
24
+echo -n "[2/4] Starting PostgreSQL 10.4 .. "
25
+# ensure statistics can be written
26
+mkdir /var/run/postgresql/10-main.pg_stat_tmp/ && chmod 777 /var/run/postgresql/10-main.pg_stat_tmp/
19 27
 # run postgres server
20
-nohup su - -c "/usr/lib/postgresql/9.5/bin/postgres -D /etc/postgresql/9.5/main" postgres > /root/postgres.log 2>&1 &
28
+nohup su - -c "/usr/lib/postgresql/10/bin/postgres -D /etc/postgresql/10/main" postgres > /root/postgres.log 2>&1 &
21 29
 # wait for postgres to become available
22 30
 until su - -c "psql -U postgres -c '\q'" postgres >/dev/null 2>&1; do
23 31
    sleep 1;
@@ -27,10 +35,22 @@ su - -c "psql -U postgres >/dev/null" postgres << 'EOF'
27 35
 CREATE USER "php-crud-api" WITH PASSWORD 'php-crud-api';
28 36
 CREATE DATABASE "php-crud-api";
29 37
 GRANT ALL PRIVILEGES ON DATABASE "php-crud-api" to "php-crud-api";
38
+\c "php-crud-api";
39
+CREATE EXTENSION IF NOT EXISTS postgis;
30 40
 \q
31 41
 EOF
42
+echo "done"
43
+
44
+echo -n "[3/4] Starting SQLServer 2017 ... "
45
+echo "skipped"
46
+
47
+echo -n "[4/4] Cloning PHP-CRUD-API v2 ... "
48
+# install software
49
+git clone --quiet https://github.com/mevdschee/php-crud-api2.git
50
+echo "done"
51
+
52
+echo "------------------------------------------------"
32 53
 
33 54
 # run the tests
34
-cd /root/php-crud-api
35
-git pull
36
-php phpunit.phar
55
+cd php-crud-api2
56
+php test.php

+ 0
- 21
examples/client.html View File

@@ -1,21 +0,0 @@
1
-<html>
2
-<head>
3
-<script src="../lib/php_crud_api_transform.js"></script>
4
-<script>
5
-var xhttp = new XMLHttpRequest();
6
-xhttp.onreadystatechange = function() {
7
-	if (this.readyState == 4 && this.status == 200) {
8
-		console.log(this.responseText);
9
-		jsonObject = php_crud_api_transform(JSON.parse(this.responseText));
10
-		document.getElementById('output').innerHTML = JSON.stringify(jsonObject, undefined, 4);
11
-	}
12
-};
13
-xhttp.open("GET", "http://localhost/api.php/posts?include=categories,tags,comments&filter=id,eq,1", true);
14
-xhttp.send();
15
-</script>
16
-</head>
17
-<body>
18
-<pre id="output"></pre>
19
-</body>
20
-</html>
21
-

+ 0
- 36
examples/client.php View File

@@ -1,36 +0,0 @@
1
-<?php
2
-require "../lib/php_crud_api_transform.php";
3
-
4
-function call($method, $url, $data = false) {
5
-	$ch = curl_init();
6
-	curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
7
-	curl_setopt($ch, CURLOPT_URL, $url);
8
-	if ($data) {
9
-		curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
10
-		$headers = array();
11
-		$headers[] = 'Content-Type: application/json';
12
-		$headers[] = 'Content-Length: ' . strlen($data);
13
-		curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
14
-	}
15
-	curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
16
-	return curl_exec($ch);
17
-}
18
-
19
-$response = call('GET', 'http://localhost/api.php/posts?include=categories');
20
-$jsonObject = json_decode($response, true);
21
-
22
-$jsonObject = php_crud_api_transform($jsonObject);
23
-$output = json_encode($jsonObject, JSON_PRETTY_PRINT);
24
-
25
-$object = array('user_id'=>1,'category_id'=>1,'content'=>'from php');
26
-call('POST', 'http://localhost/api.php/posts',json_encode($object));
27
-
28
-?>
29
-<html>
30
-<head>
31
-</head>
32
-<body>
33
-<pre><?php echo $output ?></pre>
34
-</body>
35
-</html>
36
-

+ 0
- 25
examples/client_angular.html View File

@@ -1,25 +0,0 @@
1
-<html>
2
-<head>
3
-<script src= "http://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
4
-<script src="../lib/php_crud_api_transform.js"></script>
5
-<script>
6
-var app = angular.module('myApplication', []);
7
-app.controller('postController', function($scope, $http) {
8
-	var url = '../api.php/posts';
9
-	$http.post(url, {user_id: 1, category_id: 1, content: "from angular"}).success(function() {
10
-		$http.get(url).success(function(response) {
11
-			$scope.posts = php_crud_api_transform(response).posts;
12
-		});
13
-	});
14
-});
15
-</script>
16
-</head>
17
-<body>
18
-<div ng-app="myApplication" ng-controller="postController">
19
-	<ul>
20
-		<li ng-repeat="x in posts">{{ x.id + ', ' + x.content }}</li>
21
-	</ul>
22
-</div>
23
-</body>
24
-</html>
25
-

+ 0
- 32
examples/client_angular2.html View File

@@ -1,32 +0,0 @@
1
-<html>
2
-<head>
3
-<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/2.0.0-beta.14/angular2-polyfills.min.js"></script>
4
-<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/2.0.0-beta.14/Rx.umd.min.js"></script>
5
-<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/2.0.0-beta.14/angular2-all.umd.min.js"></script>
6
-<script src="../lib/php_crud_api_transform.js"></script>
7
-<script>
8
-AppComponent =
9
-  ng.core.Component({
10
-    selector: 'my-app',
11
-    providers: [ng.http.HTTP_PROVIDERS],
12
-    template: '<ul><li *ngFor="#x of posts">{{ x.id + ", " + x.content }}</li></ul>'
13
-  })
14
-  .Class({
15
-    constructor: [
16
-      ng.http.Http, function(http) {
17
-        var url = "../api.php/posts";
18
-        http.post(url,JSON.stringify({user_id:1,category_id:1,content:"from angular2"})).subscribe();
19
-        http.get(url).map(res => php_crud_api_transform(res.json())).subscribe(res => this.posts = res.posts);
20
-      }
21
-    ]
22
-  });
23
-document.addEventListener("DOMContentLoaded", function(event) {
24
-  ng.core.enableProdMode();
25
-  ng.platform.browser.bootstrap(AppComponent);
26
-});
27
-</script>
28
-</head>
29
-<body>
30
-<my-app>Loading...</my-app>
31
-</body>
32
-</html>

+ 0
- 42
examples/client_angular2_auth.html View File

@@ -1,42 +0,0 @@
1
-<html>
2
-<head>
3
-<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/2.0.0-beta.14/angular2-polyfills.min.js"></script>
4
-<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/2.0.0-beta.14/Rx.umd.min.js"></script>
5
-<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/2.0.0-beta.14/angular2-all.umd.min.js"></script>
6
-<script src="../lib/php_crud_api_transform.js"></script>
7
-<script>
8
-AppComponent =
9
-  ng.core.Component({
10
-    selector: 'my-app',
11
-    providers: [ng.http.HTTP_PROVIDERS],
12
-    template: '<ul><li *ngFor="#x of posts">{{ x.id + ", " + x.content }}</li></ul>'
13
-  })
14
-  .Class({
15
-    constructor: [
16
-      ng.http.Http, function(http) {
17
-        // add withCredentials
18
-        let _build = http._backend._browserXHR.build;
19
-        http._backend._browserXHR.build = () => {
20
-          let _xhr =  _build();
21
-          _xhr.withCredentials = true;
22
-          return _xhr;
23
-        };
24
-        var url = "../api.php";
25
-        http.post(url,JSON.stringify({username:"admin",password:"admin"})).subscribe(res => {
26
-          url += "/posts?csrf="+JSON.parse(res._body);
27
-          http.post(url,JSON.stringify({user_id:1,category_id:1,content:"from angular2"})).subscribe();
28
-          http.get(url).map(res => php_crud_api_transform(res.json())).subscribe(res => this.posts = res.posts);
29
-        });
30
-      }
31
-    ]
32
-  });
33
-document.addEventListener("DOMContentLoaded", function(event) {
34
-  ng.core.enableProdMode();
35
-  ng.platform.browser.bootstrap(AppComponent);
36
-});
37
-</script>
38
-</head>
39
-<body>
40
-<my-app>Loading...</my-app>
41
-</body>
42
-</html>

+ 0
- 46
examples/client_auth.php View File

@@ -1,46 +0,0 @@
1
-<?php
2
-require "../lib/php_crud_api_transform.php";
3
-
4
-$cookiejar = tempnam(sys_get_temp_dir(), 'cookiejar-');
5
-
6
-function call($method, $url, $csrf = false, $data = false) {
7
-	global $cookiejar;
8
-	$ch = curl_init();
9
-	curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
10
-	curl_setopt($ch, CURLOPT_URL, $url);
11
-	$headers = array();
12
-	if ($data) {
13
-		curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
14
-		$headers[] = 'Content-Type: application/json';
15
-		$headers[] = 'Content-Length: ' . strlen($data);
16
-	}
17
-	if ($csrf) {
18
-		$headers[] = 'X-XSRF-TOKEN: ' . $csrf;
19
-	}
20
-	curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
21
-	curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
22
-
23
-	curl_setopt($ch, CURLOPT_COOKIEJAR, $cookiejar);
24
-	curl_setopt($ch, CURLOPT_COOKIEFILE, $cookiejar);
25
-
26
-	return curl_exec($ch);
27
-}
28
-
29
-// in case you are using php-api-auth:
30
-$csrf = json_decode(call('POST','http://localhost/api.php/', false, 'username=admin&password=admin'));
31
-$response = call('GET','http://localhost/api.php/posts?include=categories,tags,comments&filter=id,eq,1', $csrf);
32
-
33
-unlink($cookiejar);
34
-
35
-$jsonObject = json_decode($response,true);
36
-
37
-$jsonObject = php_crud_api_transform($jsonObject);
38
-$output = json_encode($jsonObject,JSON_PRETTY_PRINT);
39
-?>
40
-<html>
41
-<head>
42
-</head>
43
-<body>
44
-<pre><?php echo $output ?></pre>
45
-</body>
46
-</html>

+ 0
- 2
examples/client_bash.sh View File

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

+ 0
- 16
examples/client_bash_auth.sh View File

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

+ 0
- 67
examples/client_handlebars.html View File

@@ -1,67 +0,0 @@
1
-<html>
2
-<head>
3
-<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.5/handlebars.js"></script>
4
-<script src="https://cdnjs.cloudflare.com/ajax/libs/zepto/1.1.6/zepto.min.js"></script>
5
-<script src="../lib/php_crud_api_transform.js"></script>
6
-<script id="PostListTemplate" type="text/mustache">
7
-<ul>
8
-	{{#posts}}
9
-		<li>
10
-			<span class="id">{{id}}</span>, <span class="content">{{content}}</span>
11
-			<a href="javascript:void(0)" class="edit">edit</a>
12
-			<a href="javascript:void(0)" class="delete">del</a>
13
-		</li>
14
-	{{/posts}}
15
-	<li>
16
-		<form>
17
-			<input name="content"/>
18
-		</form>
19
-	</li>
20
-</ul>
21
-</script>
22
-<script>
23
-function PostList(element, template) {
24
-	var self = this;
25
-	var url = '../api.php/posts';
26
-	self.edit = function() {
27
-		var li = $(this).parent('li');
28
-		var id = li.find('span.id').text();
29
-		var content = li.find('span.content').text();
30
-		content = prompt('Value',content);
31
-		if (content!==null) {
32
-			$.ajax({url:url+'/'+id, type: 'PUT', data: {content:content}, success:self.update});
33
-		}
34
-	};
35
-	self.delete = function() {
36
-		var li = $(this).parent('li');
37
-		var id = li.find('span.id').text();
38
-		if (confirm("Deleting #"+id+". Continue?")) {
39
-			$.ajax({url:url+'/'+id, type: 'DELETE', success:self.update});
40
-		}
41
-	};
42
-	self.submit = function(e) {
43
-		e.preventDefault();
44
-		var content = $(this).find('input[name="content"]').val();
45
-		$.post(url, {user_id:1,category_id:1,content:content}, self.update);
46
-	};
47
-	self.render = function(data) {
48
-		element.html(Handlebars.compile(template.html())(php_crud_api_transform(data)));
49
-	};
50
-	self.update = function() {
51
-		$.get(url, self.render);
52
-	};
53
-	self.post = function() {
54
-		$.post(url, {user_id:1,category_id:1,content:"from handlebars"}, self.update);
55
-	};
56
-	element.on('submit','form',self.submit);
57
-	element.on('click','a.edit',self.edit)
58
-	element.on('click','a.delete',self.delete)
59
-	self.post();
60
-};
61
-$(function(){ new PostList($('#PostListDiv'),$('#PostListTemplate')); });
62
-</script>
63
-</head>
64
-<body>
65
-<div id="PostListDiv">Loading...</div>
66
-</body>
67
-</html>

+ 0
- 69
examples/client_handlebars_compiled.html View File

@@ -1,69 +0,0 @@
1
-<html>
2
-<head>
3
-<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.5/handlebars.runtime.js"></script>
4
-<script src="https://cdnjs.cloudflare.com/ajax/libs/zepto/1.1.6/zepto.min.js"></script>
5
-<script src="../lib/php_crud_api_transform.js"></script>
6
-<script>
7
-var PostListTemplate = {"1":function(container,depth0,helpers,partials,data) {
8
-    var helper;
9
-
10
-  return "		<li>\n			<span class=\"id\">"
11
-    + 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)))
12
-    + "</span>, <span class=\"content\">"
13
-    + 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)))
14
-    + "</span>\n			<a href=\"javascript:void(0)\" class=\"edit\">edit</a>\n			<a href=\"javascript:void(0)\" class=\"delete\">del</a>\n		</li>\n";
15
-},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
16
-    var stack1, helper, options, buffer =
17
-  "\n<ul>\n";
18
-  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));
19
-  if (!helpers.posts) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}
20
-  if (stack1 != null) { buffer += stack1; }
21
-  return buffer + "	<li>\n		<form>\n			<input name=\"content\"/>\n		</form>\n	</li>\n</ul>\n";
22
-},"useData":true}
23
-</script>
24
-<script>
25
-function PostList(element, template) {
26
-	var self = this;
27
-	var url = '../api.php/posts';
28
-	self.edit = function() {
29
-		var li = $(this).parent('li');
30
-		var id = li.find('span.id').text();
31
-		var content = li.find('span.content').text();
32
-		content = prompt('Value',content);
33
-		if (content!==null) {
34
-			$.ajax({url:url+'/'+id, type: 'PUT', data: {content:content}, success:self.update});
35
-		}
36
-	};
37
-	self.delete = function() {
38
-		var li = $(this).parent('li');
39
-		var id = li.find('span.id').text();
40
-		if (confirm("Deleting #"+id+". Continue?")) {
41
-			$.ajax({url:url+'/'+id, type: 'DELETE', success:self.update});
42
-		}
43
-	};
44
-	self.submit = function(e) {
45
-		e.preventDefault();
46
-		var content = $(this).find('input[name="content"]').val();
47
-		$.post(url, {user_id:1,category_id:1,content:content}, self.update);
48
-	};
49
-	self.render = function(data) {
50
-		element.html(Handlebars.template(PostListTemplate)(php_crud_api_transform(data)));
51
-	};
52
-	self.update = function() {
53
-		$.get(url, self.render);
54
-	};
55
-	self.post = function() {
56
-		$.post(url, {user_id:1,category_id:1,content:"from handlebars_compiled"}, self.update);
57
-	};
58
-	element.on('submit','form',self.submit);
59
-	element.on('click','a.edit',self.edit)
60
-	element.on('click','a.delete',self.delete)
61
-	self.post();
62
-};
63
-$(function(){ new PostList($('#PostListDiv'),$('#PostListTemplate')); });
64
-</script>
65
-</head>
66
-<body>
67
-<div id="PostListDiv">Loading...</div>
68
-</body>
69
-</html>

+ 0
- 71
examples/client_knockout.html View File

@@ -1,71 +0,0 @@
1
-<html>
2
-<head>
3
-<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
4
-<script src="https://cdnjs.cloudflare.com/ajax/libs/zepto/1.1.6/zepto.min.js"></script>
5
-<script src="../lib/php_crud_api_transform.js"></script>
6
-<script id="posts-template" type="text/html">
7
-<ul>
8
-	<!-- ko foreach: posts -->
9
-	  <li>
10
-			<span data-bind="text: id"></span>, <span data-bind="text: content"></span>
11
-			<a href="javascript:void(0)" data-bind="click: $parent.edit">edit</a>
12
-			<a href="javascript:void(0)" data-bind="click: $parent.delete">del</a>
13
-		</li>
14
-	<!-- /ko -->
15
-	<li>
16
-		<form data-bind="submit: submit">
17
-			<input name="content" data-bind="value: form"/>
18
-		</form>
19
-	</li>
20
-</ul>
21
-</script>
22
-<script>
23
-var url = '../api.php/posts';
24
-function Post(id,content){
25
-	var self = this;
26
-	self.id = ko.observable(id);
27
-	self.content = ko.observable(content);
28
-}
29
-function PostList(){
30
-	var self = this;
31
-	self.posts = ko.observableArray([]);
32
-	self.form = ko.observable('');
33
-	self.bound = false;
34
-	self.edit = function(post) {
35
-		var content = prompt('Value',post.content());
36
-		if (content!==null) {
37
-			$.ajax({url:url+'/'+post.id(), type: 'PUT', data: {content:content}, success:self.update});
38
-		}
39
-	};
40
-	self.delete = function(post) {
41
-		if (confirm("Deleting #"+post.id()+". Continue?")) {
42
-			$.ajax({url:url+'/'+post.id(), type: 'DELETE', success:self.update});
43
-		}
44
-	};
45
-	self.submit = function(form) {
46
-		$.post(url, {user_id:1,category_id:1,content:self.form()}, self.update);
47
-	};
48
-	self.render = function(data) {
49
-		var array = php_crud_api_transform(data).posts;
50
-		self.posts.removeAll();
51
-		for (i=0;i<array.length;i++) {
52
-			self.posts.push(new Post(array[i].id,array[i].content));
53
-		}
54
-		self.form('');
55
-		if (!self.bound){ ko.applyBindings(self); self.bound = true; }
56
-	};
57
-	self.update = function() {
58
-		$.get(url, self.render);
59
-	};
60
-	self.post = function() {
61
-		$.post(url, {user_id:1,category_id:1,content:"from knockout"}, self.update);
62
-	}
63
-	self.post();
64
-};
65
-$(function(){	new PostList(); });
66
-</script>
67
-</head>
68
-<body>
69
-<div data-bind="template: { name: 'posts-template'}">Loading...</div>
70
-</body>
71
-</html>

+ 0
- 67
examples/client_mustache.html View File

@@ -1,67 +0,0 @@
1
-<html>
2
-<head>
3
-<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.2.1/mustache.min.js"></script>
4
-<script src="https://cdnjs.cloudflare.com/ajax/libs/zepto/1.1.6/zepto.min.js"></script>
5
-<script src="../lib/php_crud_api_transform.js"></script>
6
-<script id="PostListTemplate" type="text/mustache">
7
-<ul>
8
-	{{#posts}}
9
-		<li>
10
-			<span class="id">{{id}}</span>, <span class="content">{{content}}</span>
11
-			<a href="javascript:void(0)" class="edit">edit</a>
12
-			<a href="javascript:void(0)" class="delete">del</a>
13
-		</li>
14
-	{{/posts}}
15
-	<li>
16
-		<form>
17
-			<input name="content"/>
18
-		</form>
19
-	</li>
20
-</ul>
21
-</script>
22
-<script>
23
-function PostList(element, template) {
24
-	var self = this;
25
-	var url = '../api.php/posts';
26
-	self.edit = function() {
27
-		var li = $(this).parent('li');
28
-		var id = li.find('span.id').text();
29
-		var content = li.find('span.content').text();
30
-		content = prompt('Value',content);
31
-		if (content!==null) {
32
-			$.ajax({url:url+'/'+id, type: 'PUT', data: {content:content}, success:self.update});
33
-		}
34
-	};
35
-	self.delete = function() {
36
-		var li = $(this).parent('li');
37
-		var id = li.find('span.id').text();
38
-		if (confirm("Deleting #"+id+". Continue?")) {
39
-			$.ajax({url:url+'/'+id, type: 'DELETE', success:self.update});
40
-		}
41
-	};
42
-	self.submit = function(e) {
43
-		e.preventDefault();
44
-		var content = $(this).find('input[name="content"]').val();
45
-		$.post(url, {user_id:1,category_id:1,content:content}, self.update);
46
-	};
47
-	self.render = function(data) {
48
-		element.html(Mustache.to_html(template.html(),php_crud_api_transform(data)));
49
-	};
50
-	self.update = function() {
51
-		$.get(url, self.render);
52
-	};
53
-	self.post = function() {
54
-		$.post(url, {user_id:1,category_id:1,content:"from mustache"}, self.update);
55
-	};
56
-	element.on('submit','form',self.submit);
57
-	element.on('click','a.edit',self.edit)
58
-	element.on('click','a.delete',self.delete)
59
-	self.post();
60
-};
61
-$(function(){ new PostList($('#PostListDiv'),$('#PostListTemplate')); });
62
-</script>
63
-</head>
64
-<body>
65
-<div id="PostListDiv">Loading...</div>
66
-</body>
67
-</html>

+ 0
- 48
examples/client_react.html View File

@@ -1,48 +0,0 @@
1
-<html>
2
-<head>
3
-<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.0/react.js"></script>
4
-<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.0/react-dom.js"></script>
5
-<script src="https://cdnjs.cloudflare.com/ajax/libs/zepto/1.1.6/zepto.min.js"></script>
6
-<script src="../lib/php_crud_api_transform.js"></script>
7
-<script>
8
-var PostList = React.createClass({
9
-  displayName: 'PostList',
10
-  url: '../api.php/posts',
11
-  getInitialState: function() {
12
-    return { posts: [] };
13
-  },
14
-  retrieveServerState: function() {
15
-    this.serverRequest = $.get(this.url, function (data) {
16
-      this.setState(php_crud_api_transform(data));
17
-    }.bind(this));
18
-  },
19
-  componentDidMount: function() {
20
-    $.post(this.url, {user_id:1,category_id:1,content:"from react"}, this.retrieveServerState);
21
-  },
22
-  componentWillUnmount: function() {
23
-    this.serverRequest.abort();
24
-  },
25
-  render: function render() {
26
-    var createPost = function(post) {
27
-      return React.createElement(
28
-        'li',
29
-        {key: post.id},
30
-        post.id,
31
-        ', ',
32
-        post.content
33
-      );
34
-    };
35
-    return React.createElement(
36
-      'ul',
37
-      null,
38
-      this.state.posts.map(createPost)
39
-    );
40
-  }
41
-});
42
-$(function(){ ReactDOM.render(React.createElement(PostList, null), document.getElementById('myApplication')); });
43
-</script>
44
-</head>
45
-<body>
46
-<div id="myApplication">Loading...</div>
47
-</body>
48
-</html>

+ 0
- 251
examples/client_vue.html View File

@@ -1,251 +0,0 @@
1
-<html lang="en">
2
-<head>
3
-  <meta charset="utf-8">
4
-  <meta http-equiv="x-ua-compatible" content="ie=edge">
5
-  <title>Vue.js CRUD application</title>
6
-  <meta name="description" content="">
7
-  <meta name="viewport" content="width=device-width, initial-scale=1">
8
-  <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.js"></script>
9
-  <script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/2.2.1/vue-router.js"></script>
10
-  <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.15.3/axios.js"></script>
11
-  <script src="../lib/php_crud_api_transform.js"></script>
12
-  <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
13
-  <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap-theme.min.css">
14
-<style>
15
-.logo {
16
-  width: 50px;
17
-  float: left;
18
-  margin-right: 15px;
19
-}
20
-
21
-.form-group {
22
-  max-width: 500px;
23
-}
24
-
25
-.actions {
26
-  padding: 10px 0;
27
-}
28
-
29
-.glyphicon-euro {
30
-  font-size: 12px;
31
-}
32
-</style>
33
-</head>
34
-<body>
35
-
36
-<div class="container">
37
-  <header class="page-header">
38
-    <div class="branding">
39
-      <img src="https://vuejs.org/images/logo.png" alt="Logo" title="Home page" class="logo"/>
40
-      <h1>Vue.js CRUD application</h1>
41
-    </div>
42
-  </header>
43
-  <main id="app">
44
-    <router-view></router-view>
45
-  </main>
46
-</div>
47
-
48
-<template id="post-list">
49
-  <div>
50
-    <div class="actions">
51
-      <router-link class="btn btn-default" v-bind:to="{path: '/add-post'}">
52
-        <span class="glyphicon glyphicon-plus"></span>
53
-        Add post
54
-      </router-link>
55
-    </div>
56
-    <div class="filters row">
57
-      <div class="form-group col-sm-3">
58
-        <label for="search-element">Filter</label>
59
-        <input v-model="searchKey" class="form-control" id="search-element" requred/>
60
-      </div>
61
-    </div>
62
-    <table class="table">
63
-      <thead>
64
-      <tr>
65
-        <th>Content</th>
66
-        <th class="col-sm-2">Actions</th>
67
-      </tr>
68
-      </thead>
69
-      <tbody>
70
-      <tr v-if="posts===null">
71
-        <td colspan="4">Loading...</td>
72
-      </tr>
73
-      <tr v-else v-for="post in filteredposts">
74
-        <td>
75
-          <router-link v-bind:to="{name: 'post', params: {post_id: post.id}}">{{ post.content }}</router-link>
76
-        </td>
77
-        <td>
78
-          <router-link class="btn btn-warning btn-xs" v-bind:to="{name: 'post-edit', params: {post_id: post.id}}">Edit</router-link>
79
-          <router-link class="btn btn-danger btn-xs" v-bind:to="{name: 'post-delete', params: {post_id: post.id}}">Delete</router-link>
80
-        </td>
81
-      </tr>
82
-      </tbody>
83
-    </table>
84
-  </div>
85
-</template>
86
-
87
-<template id="add-post">
88
-  <div>
89
-    <h2>Add new post</h2>
90
-    <form v-on:submit="createpost">
91
-      <div class="form-group">
92
-        <label for="add-content">Content</label>
93
-        <textarea class="form-control" id="add-content" rows="10" v-model="post.content"></textarea>
94
-      </div>
95
-      <button type="submit" class="btn btn-primary">Create</button>
96
-      <router-link class="btn btn-default" v-bind:to="'/'">Cancel</router-link>
97
-    </form>
98
-  </div>
99
-</template>
100
-
101
-<template id="post">
102
-  <div>
103
-    <b>Content: </b>
104
-    <div>{{ post.content }}</div>
105
-    <br/>
106
-    <span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span>
107
-    <router-link v-bind:to="'/'">Back to post list</router-link>
108
-  </div>
109
-</template>
110
-
111
-<template id="post-edit">
112
-  <div>
113
-    <h2>Edit post</h2>
114
-    <form v-on:submit="updatepost">
115
-      <div class="form-group">
116
-        <label for="edit-content">Content</label>
117
-        <textarea class="form-control" id="edit-content" rows="3" v-model="post.content"></textarea>
118
-      </div>
119
-      <button type="submit" class="btn btn-primary">Save</button>
120
-      <router-link class="btn btn-default" v-bind:to="'/'">Cancel</router-link>
121
-    </form>
122
-  </div>
123
-</template>
124
-
125
-<template id="post-delete">
126
-  <div>
127
-    <h2>Delete post {{ post.id }}</h2>
128
-    <form v-on:submit="deletepost">
129
-      <p>The action cannot be undone.</p>
130
-      <button type="submit" class="btn btn-danger">Delete</button>
131
-      <router-link class="btn btn-default" v-bind:to="'/'">Cancel</router-link>
132
-    </form>
133
-  </div>
134
-</template>
135
-
136
-<script>
137
-var posts = null;
138
-
139
-var api = axios.create({
140
-  baseURL: '../api.php'
141
-});
142
-
143
-function findpost (postId) {
144
-  return posts[findpostKey(postId)];
145
-};
146
-
147
-function findpostKey (postId) {
148
-  for (var key = 0; key < posts.length; key++) {
149
-    if (posts[key].id == postId) {
150
-      return key;
151
-    }
152
-  }
153
-};
154
-
155
-var List = Vue.extend({
156
-  template: '#post-list',
157
-  data: function () {
158
-    return {posts: posts, searchKey: ''};
159
-  },
160
-  created: function () {
161
-    var self = this;
162
-    api.get('/posts').then(function (response) {
163
-      posts = self.posts = php_crud_api_transform(response.data).posts;
164
-    }).catch(function (error) {
165
-      console.log(error);
166
-    });
167
-  },
168
-  computed: {
169
-    filteredposts: function () {
170
-      return this.posts.filter(function (post) {
171
-        return this.searchKey=='' || post.content.indexOf(this.searchKey) !== -1;
172
-      },this);
173
-    }
174
-  }
175
-});
176
-
177
-var post = Vue.extend({
178
-  template: '#post',
179
-  data: function () {
180
-    return {post: findpost(this.$route.params.post_id)};
181
-  }
182
-});
183
-
184
-var postEdit = Vue.extend({
185
-  template: '#post-edit',
186
-  data: function () {
187
-    return {post: findpost(this.$route.params.post_id)};
188
-  },
189
-  methods: {
190
-    updatepost: function () {
191
-      var post = this.post;
192
-      api.put('/posts/'+post.id,post).then(function (response) {
193
-        console.log(response.data);
194
-      }).catch(function (error) {
195
-        console.log(error);
196
-      });
197
-      router.push('/');
198
-    }
199
-  }
200
-});
201
-
202
-var postDelete = Vue.extend({
203
-  template: '#post-delete',
204
-  data: function () {
205
-    return {post: findpost(this.$route.params.post_id)};
206
-  },
207
-  methods: {
208
-    deletepost: function () {
209
-      var post = this.post;
210
-      api.delete('/posts/'+post.id).then(function (response) {
211
-        console.log(response.data);
212
-      }).catch(function (error) {
213
-        console.log(error);
214
-      });
215
-      router.push('/');
216
-    }
217
-  }
218
-});
219
-
220
-var Addpost = Vue.extend({
221
-  template: '#add-post',
222
-  data: function () {
223
-    return {post: {content: '', user_id: 1, category_id: 1}}
224
-  },
225
-  methods: {
226
-    createpost: function() {
227
-      var post = this.post;
228
-      api.post('/posts',post).then(function (response) {
229
-        post.id = response.data;
230
-      }).catch(function (error) {
231
-        console.log(error);
232
-      });
233
-      router.push('/');
234
-    }
235
-  }
236
-});
237
-
238
-var router = new VueRouter({routes:[
239
-  { path: '/', component: List},
240
-  { path: '/post/:post_id', component: post, name: 'post'},
241
-  { path: '/add-post', component: Addpost},
242
-  { path: '/post/:post_id/edit', component: postEdit, name: 'post-edit'},
243
-  { path: '/post/:post_id/delete', component: postDelete, name: 'post-delete'}
244
-]});
245
-app = new Vue({
246
-  router:router
247
-}).$mount('#app')
248
-</script>
249
-
250
-</body>
251
-</html>

+ 0
- 263
examples/client_vue_auth.html View File

@@ -1,263 +0,0 @@
1
-<html lang="en">
2
-<head>
3
-  <meta charset="utf-8">
4
-  <meta http-equiv="x-ua-compatible" content="ie=edge">
5
-  <title>Vue.js CRUD application</title>
6
-  <meta name="description" content="">
7
-  <meta name="viewport" content="width=device-width, initial-scale=1">
8
-  <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.js"></script>
9
-  <script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/2.2.1/vue-router.js"></script>
10
-  <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.15.3/axios.js"></script>
11
-  <script src="../lib/php_crud_api_transform.js"></script>
12
-  <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
13
-  <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap-theme.min.css">
14
-<style>
15
-.logo {
16
-  width: 50px;
17
-  float: left;
18
-  margin-right: 15px;
19
-}
20
-
21
-.form-group {
22
-  max-width: 500px;
23
-}
24
-
25
-.actions {
26
-  padding: 10px 0;
27
-}
28
-
29
-.glyphicon-euro {
30
-  font-size: 12px;
31
-}
32
-</style>
33
-</head>
34
-<body>
35
-
36
-<div class="container">
37
-  <header class="page-header">
38
-    <div class="branding">
39
-      <img src="https://vuejs.org/images/logo.png" alt="Logo" title="Home page" class="logo"/>
40
-      <h1>Vue.js CRUD application</h1>
41
-    </div>
42
-  </header>
43
-  <main id="app">
44
-    <router-view></router-view>
45
-  </main>
46
-</div>
47
-
48
-<template id="post-list">
49
-  <div>
50
-    <div class="actions">
51
-      <router-link class="btn btn-default" v-bind:to="{path: '/add-post'}">
52
-        <span class="glyphicon glyphicon-plus"></span>
53
-        Add post
54
-      </router-link>
55
-    </div>
56
-    <div class="filters row">
57
-      <div class="form-group col-sm-3">
58
-        <label for="search-element">Filter</label>
59
-        <input v-model="searchKey" class="form-control" id="search-element" requred/>
60
-      </div>
61
-    </div>
62
-    <table class="table">
63
-      <thead>
64
-      <tr>
65
-        <th>Content</th>
66
-        <th class="col-sm-2">Actions</th>
67
-      </tr>
68
-      </thead>
69
-      <tbody>
70
-      <tr v-if="posts===null">
71
-        <td colspan="4">Loading...</td>
72
-      </tr>
73
-      <tr v-else v-for="post in filteredposts">
74
-        <td>
75
-          <router-link v-bind:to="{name: 'post', params: {post_id: post.id}}">{{ post.content }}</router-link>
76
-        </td>
77
-        <td>
78
-          <router-link class="btn btn-warning btn-xs" v-bind:to="{name: 'post-edit', params: {post_id: post.id}}">Edit</router-link>
79
-          <router-link class="btn btn-danger btn-xs" v-bind:to="{name: 'post-delete', params: {post_id: post.id}}">Delete</router-link>
80
-        </td>
81
-      </tr>
82
-      </tbody>
83
-    </table>
84
-  </div>
85
-</template>
86
-
87
-<template id="add-post">
88
-  <div>
89
-    <h2>Add new post</h2>
90
-    <form v-on:submit="createpost">
91
-      <div class="form-group">
92
-        <label for="add-content">Content</label>
93
-        <textarea class="form-control" id="add-content" rows="10" v-model="post.content"></textarea>
94
-      </div>
95
-      <button type="submit" class="btn btn-primary">Create</button>
96
-      <router-link class="btn btn-default" v-bind:to="'/'">Cancel</router-link>
97
-    </form>
98
-  </div>
99
-</template>
100
-
101
-<template id="post">
102
-  <div>
103
-    <b>Content: </b>
104
-    <div>{{ post.content }}</div>
105
-    <br/>
106
-    <span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span>
107
-    <router-link v-bind:to="'/'">Back to post list</router-link>
108
-  </div>
109
-</template>
110
-
111
-<template id="post-edit">
112
-  <div>
113
-    <h2>Edit post</h2>
114
-    <form v-on:submit="updatepost">
115
-      <div class="form-group">
116
-        <label for="edit-content">Content</label>
117
-        <textarea class="form-control" id="edit-content" rows="3" v-model="post.content"></textarea>
118
-      </div>
119
-      <button type="submit" class="btn btn-primary">Save</button>
120
-      <router-link class="btn btn-default" v-bind:to="'/'">Cancel</router-link>
121
-    </form>
122
-  </div>
123
-</template>
124
-
125
-<template id="post-delete">
126
-  <div>
127
-    <h2>Delete post {{ post.id }}</h2>
128
-    <form v-on:submit="deletepost">
129
-      <p>The action cannot be undone.</p>
130
-      <button type="submit" class="btn btn-danger">Delete</button>
131
-      <router-link class="btn btn-default" v-bind:to="'/'">Cancel</router-link>
132
-    </form>
133
-  </div>
134
-</template>
135
-
136
-<script>
137
-var posts = null;
138
-
139
-var api = axios.create({
140
-  baseURL: '../api.php',
141
-  withCredentials: true
142
-});
143
-
144
-api.interceptors.response.use(function (response) {
145
-  if (response.headers['x-xsrf-token']) {
146
-    document.cookie = 'XSRF-TOKEN=' + response.headers['x-xsrf-token'] + '; path=/';
147
-  }
148
-  return response;
149
-});
150
-
151
-function findpost (postId) {
152
-  return posts[findpostKey(postId)];
153
-};
154
-
155
-function findpostKey (postId) {
156
-  for (var key = 0; key < posts.length; key++) {
157
-    if (posts[key].id == postId) {
158
-      return key;
159
-    }
160
-  }
161
-};
162
-
163
-var List = Vue.extend({
164
-  template: '#post-list',
165
-  data: function () {
166
-    return {posts: posts, searchKey: ''};
167
-  },
168
-  created: function () {
169
-    var self = this;
170
-    api.post('/',{username:"admin",password:"admin"}).then(function (response) {
171
-      api.get('/posts').then(function (response) {
172
-        posts = self.posts = php_crud_api_transform(response.data).posts;
173
-      }).catch(function (error) {
174
-        console.log(error);
175
-      });
176
-    }).catch(function (error) {
177
-      console.log(error);
178
-    });
179
-  },
180
-  computed: {
181
-    filteredposts: function () {
182
-      return this.posts.filter(function (post) {
183
-        return this.searchKey=='' || post.content.indexOf(this.searchKey) !== -1;
184
-      },this);
185
-    }
186
-  }
187
-});
188
-
189
-var post = Vue.extend({
190
-  template: '#post',
191
-  data: function () {
192
-    return {post: findpost(this.$route.params.post_id)};
193
-  }
194
-});
195
-
196
-var postEdit = Vue.extend({
197
-  template: '#post-edit',
198
-  data: function () {
199
-    return {post: findpost(this.$route.params.post_id)};
200
-  },
201
-  methods: {
202
-    updatepost: function () {
203
-      var post = this.post;
204
-      api.put('/posts/'+post.id,post).then(function (response) {
205
-        console.log(response.data);
206
-      }).catch(function (error) {
207
-        console.log(error);
208
-      });
209
-      router.push('/');
210
-    }
211
-  }
212
-});
213
-
214
-var postDelete = Vue.extend({
215
-  template: '#post-delete',
216
-  data: function () {
217
-    return {post: findpost(this.$route.params.post_id)};
218
-  },
219
-  methods: {
220
-    deletepost: function () {
221
-      var post = this.post;
222
-      api.delete('/posts/'+post.id).then(function (response) {
223
-        console.log(response.data);
224
-      }).catch(function (error) {
225
-        console.log(error);
226
-      });
227
-      router.push('/');
228
-    }
229
-  }
230
-});
231
-
232
-var Addpost = Vue.extend({
233
-  template: '#add-post',
234
-  data: function () {
235
-    return {post: {content: '', user_id: 1, category_id: 1}}
236
-  },
237
-  methods: {
238
-    createpost: function() {
239
-      var post = this.post;
240
-      api.post('/posts',post).then(function (response) {
241
-        post.id = response.data;
242
-      }).catch(function (error) {
243
-        console.log(error);
244
-      });
245
-      router.push('/');
246
-    }
247
-  }
248
-});
249
-
250
-var router = new VueRouter({routes:[
251
-  { path: '/', component: List},
252
-  { path: '/post/:post_id', component: post, name: 'post'},
253
-  { path: '/add-post', component: Addpost},
254
-  { path: '/post/:post_id/edit', component: postEdit, name: 'post-edit'},
255
-  { path: '/post/:post_id/delete', component: postDelete, name: 'post-delete'}
256
-]});
257
-app = new Vue({
258
-  router:router
259
-}).$mount('#app')
260
-</script>
261
-
262
-</body>
263
-</html>

+ 0
- 77
examples/client_zepto.html View File

@@ -1,77 +0,0 @@
1
-<html>
2
-<head>
3
-<script src="https://cdnjs.cloudflare.com/ajax/libs/zepto/1.1.6/zepto.min.js"></script>
4
-<script src="../lib/php_crud_api_transform.js"></script>
5
-<script id="PostListTemplate" type="text/html">
6
-	<ul>
7
-		<li>
8
-			<span class="id"></span>, <span class="content"></span>
9
-			<a href="javascript:void(0)" class="edit">edit</a>
10
-			<a href="javascript:void(0)" class="delete">del</a>
11
-		</li>
12
-		<li>
13
-			<form>
14
-				<input name="content"/>
15
-			</form>
16
-		</li>
17
-	</ul>
18
-</script>
19
-<script>
20
-function PostList(element,template) {
21
-	var self = this;
22
-	var url = '../api.php/posts';
23
-	self.edit = function() {
24
-		var li = $(this).parent('li');
25
-		var id = li.find('span.id').text();
26
-		var content = li.find('span.content').text();
27
-		content = prompt('Value', content);
28
-		if (content!==null) {
29
-			$.ajax({url: url + '/' + id, type: 'PUT', data: {content: content}, success: self.update});
30
-		}
31
-	};
32
-	self.delete = function() {
33
-		var li = $(this).parent('li');
34
-		var id = li.find('span.id').text();
35
-		if (confirm("Deleting #" + id + ". Continue?")) {
36
-			$.ajax({url: url + '/' + id, type: 'DELETE', success: self.update});
37
-		}
38
-	};
39
-	self.submit = function(e) {
40
-		e.preventDefault();
41
-		var content = $(this).find('input[name="content"]').val();
42
-		$.post(url, {user_id: 1, category_id: 1, content: content}, self.update);
43
-	};
44
-	self.render = function(data) {
45
-		data = php_crud_api_transform(data);
46
-		element.html(template.html());
47
-		var item = element.find('li').first().remove();
48
-		for (var i=0;i<data.posts.length; i++) {
49
-			var clone = item.clone();
50
-			clone.find('span').each(function(){
51
-				var field = $(this).attr("class");
52
-				$(this).text(data.posts[i][field]);
53
-			});
54
-			clone.insertBefore(element.find('li').last());
55
-		}
56
-	};
57
-	self.update = function() {
58
-		$.get(url, self.render);
59
-	};
60
-	self.post = function() {
61
-		$.post(url, {user_id: 1, category_id: 1, content: "from zepto"}, self.update);
62
-	};
63
-	element.on('submit', 'form', self.submit);
64
-	element.on('click', 'a.edit', self.edit);
65
-	element.on('click', 'a.delete', self.delete);
66
-	self.post();
67
-};
68
-$(function(){
69
-	new PostList($('#PostListDiv'), $('#PostListTemplate'));
70
-});
71
-</script>
72
-</head>
73
-<body>
74
-<div id="PostListDiv">Loading...</div>
75
-</body>
76
-</html>
77
-

+ 0
- 66
extras/core.php View File

@@ -1,66 +0,0 @@
1
-<?php
2
-
3
-// get the HTTP method, path and body of the request
4
-$method = $_SERVER['REQUEST_METHOD'];
5
-$request = explode('/', trim($_SERVER['PATH_INFO'],'/'));
6
-$input = json_decode(file_get_contents('php://input'),true);
7
-if (!$input) $input = array();
8
-
9
-// connect to the mysql database
10
-$link = mysqli_connect('localhost', 'php-crud-api', 'php-crud-api', 'php-crud-api');
11
-mysqli_set_charset($link,'utf8');
12
-
13
-// retrieve the table and key from the path
14
-$table = preg_replace('/[^a-z0-9_]+/i','',array_shift($request));
15
-$key = array_shift($request)+0;
16
-
17
-// escape the columns and values from the input object
18
-$columns = preg_replace('/[^a-z0-9_]+/i','',array_keys($input));
19
-$values = array_map(function ($value) use ($link) {
20
-  if ($value===null) return null;
21
-  return mysqli_real_escape_string($link,(string)$value);
22
-},array_values($input));
23
-
24
-// build the SET part of the SQL command
25
-$set = '';
26
-for ($i=0;$i<count($columns);$i++) {
27
-  $set.=($i>0?',':'').'`'.$columns[$i].'`=';
28
-  $set.=($values[$i]===null?'NULL':'"'.$values[$i].'"');
29
-}
30
-
31
-// create SQL based on HTTP method
32
-switch ($method) {
33
-  case 'GET':
34
-    $sql = "select * from `$table`".($key?" WHERE id=$key":''); break;
35
-  case 'PUT':
36
-    $sql = "update `$table` set $set where id=$key"; break;
37
-  case 'POST':
38
-    $sql = "insert into `$table` set $set"; break;
39
-  case 'DELETE':
40
-    $sql = "delete from `$table` where id=$key"; break;
41
-}
42
-
43
-// execute SQL statement
44
-$result = mysqli_query($link,$sql);
45
-
46
-// die if SQL statement failed
47
-if (!$result) {
48
-  http_response_code(404);
49
-  die(mysqli_error($link));
50
-}
51
-
52
-// print results, insert id or affected row count
53
-if ($method == 'GET') {
54
-  if (!$key) echo '[';
55
-  for ($i=0;$i<mysqli_num_rows($result);$i++) {
56
-    echo ($i>0?',':'').json_encode(mysqli_fetch_object($result));
57
-  }
58
-  if (!$key) echo ']';
59
-} elseif ($method == 'POST') {
60
-  echo mysqli_insert_id($link);
61
-} else {
62
-  echo mysqli_affected_rows($link);
63
-}
64
-
65
-// close mysql connection
66
-mysqli_close($link);

+ 0
- 45
lib/php_crud_api_transform.js View File

@@ -1,45 +0,0 @@
1
-function php_crud_api_transform(tables) {
2
-	var array_flip = function (trans) {
3
-		var key, tmp_ar = {};
4
-		for (key in trans) {
5
-			tmp_ar[trans[key]] = key;
6
-		}
7
-		return tmp_ar;
8
-	};
9
-	var get_objects = function (tables,table_name,where_index,match_value) {
10
-		var objects = [];
11
-		for (var record in tables[table_name]['records']) {
12
-			record = tables[table_name]['records'][record];
13
-			if (!where_index || record[where_index]==match_value) {
14
-				var object = {};
15
-				for (var index in tables[table_name]['columns']) {
16
-					var column = tables[table_name]['columns'][index];
17
-					object[column] = record[index];
18
-					for (var relation in tables) {
19
-						var reltable = tables[relation];
20
-						for (var key in reltable['relations']) {
21
-							var target = reltable['relations'][key];
22
-							if (target == table_name+'.'+column) {
23
-								column_indices = array_flip(reltable['columns']);
24
-								object[relation] = get_objects(tables,relation,column_indices[key],record[index]);
25
-							}
26
-						}
27
-					}
28
-				}
29
-				objects.push(object);
30
-			}
31
-		}
32
-		return objects;
33
-	};
34
-	tree = {};
35
-	for (var name in tables) {
36
-		var table = tables[name];
37
-		if (!table['relations']) {
38
-			tree[name] = get_objects(tables,name);
39
-			if (table['results']) {
40
-				tree['_results'] = table['results'];
41
-			}
42
-		}
43
-	}
44
-	return tree;
45
-}

+ 0
- 38
lib/php_crud_api_transform.php View File

@@ -1,38 +0,0 @@
1
-<?php
2
-function php_crud_api_transform(&$tables) {
3
-	$get_objects = function (&$tables,$table_name,$where_index=false,$match_value=false) use (&$get_objects) {
4
-		$objects = array();
5
-		if (isset($tables[$table_name]['records'])) {
6
-			foreach ($tables[$table_name]['records'] as $record) {
7
-				if ($where_index===false || $record[$where_index]==$match_value) {
8
-					$object = array();
9
-					foreach ($tables[$table_name]['columns'] as $index=>$column) {
10
-						$object[$column] = $record[$index];
11
-						foreach ($tables as $relation=>$reltable) {
12
-							if (isset($reltable['relations'])) {
13
-								foreach ($reltable['relations'] as $key=>$target) {
14
-									if ($target == "$table_name.$column") {
15
-										$column_indices = array_flip($reltable['columns']);
16
-										$object[$relation] = $get_objects($tables,$relation,$column_indices[$key],$record[$index]);
17
-									}
18
-								}
19
-							}
20
-						}
21
-					}
22
-					$objects[] = $object;
23
-				}
24
-			}
25
-		}
26
-		return $objects;
27
-	};
28
-	$tree = array();
29
-	foreach ($tables as $name=>$table) {
30
-		if (!isset($table['relations'])) {
31
-			$tree[$name] = $get_objects($tables,$name);
32
-			if (isset($table['results'])) {
33
-				$tree['_results'] = $table['results'];
34
-			}
35
-		}
36
-	}
37
-	return $tree;
38
-}

+ 0
- 35
lib/php_crud_api_transform.py View File

@@ -1,35 +0,0 @@
1
-def php_crud_api_transform(tables):
2
-
3
-    def get_objects(tables, table_name, where_index=None, match_value=None):
4
-        objects = []
5
-        for record in tables[table_name]['records']:
6
-            if where_index == None or (record[where_index] == match_value):
7
-                object = {}
8
-                columns = tables[table_name]['columns']
9
-                for column in columns:
10
-                    index = columns.index(column)
11
-                    object[column] = record[index]
12
-                    for relation, reltable in tables.items():
13
-                        for key, target in reltable.get('relations', {}).items():
14
-                            if target == table_name + '.' + column:
15
-                                relcols = reltable['columns']
16
-                                column_indices = {value: relcols.index(value) for value in relcols}
17
-                                object[relation] = get_objects(
18
-                                    tables, relation, column_indices[key], record[index])
19
-                objects.append(object)
20
-        return objects
21
-
22
-    tree = {}
23
-    for name, table in tables.items():
24
-        if not 'relations' in table:
25
-            tree[name] = get_objects(tables, name)
26
-            if 'results' in table:
27
-                tree['_results'] = table['results']
28
-    return tree
29
-
30
-
31
-if __name__ == "__main__":
32
-    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"]]}}
33
-    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"}]}
34
-
35
-    print(php_crud_api_transform(input)  == output)

+ 0
- 8
phpunit.xml View File

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

+ 112
- 0
src/Tqdev/PhpCrudApi/Api.php View File

@@ -0,0 +1,112 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi;
3
+
4
+use Tqdev\PhpCrudApi\Cache\CacheFactory;
5
+use Tqdev\PhpCrudApi\Column\DefinitionService;
6
+use Tqdev\PhpCrudApi\Column\ReflectionService;
7
+use Tqdev\PhpCrudApi\Controller\CacheController;
8
+use Tqdev\PhpCrudApi\Controller\ColumnController;
9
+use Tqdev\PhpCrudApi\Controller\OpenApiController;
10
+use Tqdev\PhpCrudApi\Controller\RecordController;
11
+use Tqdev\PhpCrudApi\Controller\Responder;
12
+use Tqdev\PhpCrudApi\Database\GenericDB;
13
+use Tqdev\PhpCrudApi\Middleware\CorsMiddleware;
14
+use Tqdev\PhpCrudApi\Middleware\FirewallMiddleware;
15
+use Tqdev\PhpCrudApi\Middleware\Router\SimpleRouter;
16
+use Tqdev\PhpCrudApi\Middleware\SanitationMiddleware;
17
+use Tqdev\PhpCrudApi\Middleware\ValidationMiddleware;
18
+use Tqdev\PhpCrudApi\OpenApi\OpenApiService;
19
+use Tqdev\PhpCrudApi\Record\ErrorCode;
20
+use Tqdev\PhpCrudApi\Record\RecordService;
21
+
22
+class Api
23
+{
24
+    private $router;
25
+    private $responder;
26
+    private $debug;
27
+
28
+    public function __construct(Config $config)
29
+    {
30
+        $db = new GenericDB(
31
+            $config->getDriver(),
32
+            $config->getAddress(),
33
+            $config->getPort(),
34
+            $config->getDatabase(),
35
+            $config->getUsername(),
36
+            $config->getPassword()
37
+        );
38
+        $cache = CacheFactory::create($config);
39
+        $reflection = new ReflectionService($db, $cache, $config->getCacheTime());
40
+        $definition = new DefinitionService($db, $reflection);
41
+        $responder = new Responder();
42
+        $router = new SimpleRouter($responder);
43
+        foreach ($config->getMiddlewares() as $middleware => $properties) {
44
+            switch ($middleware) {
45
+                case 'cors':
46
+                    new CorsMiddleware($router, $responder, $properties);
47
+                    break;
48
+                case 'firewall':
49
+                    new FirewallMiddleware($router, $responder, $properties);
50
+                    break;
51
+                case 'basicAuth':
52
+                    new BasicAuthMiddleware($router, $responder, $properties);
53
+                    break;
54
+                case 'validation':
55
+                    new ValidationMiddleware($router, $responder, $properties, $reflection);
56
+                    break;
57
+                case 'sanitation':
58
+                    new SanitationMiddleware($router, $responder, $properties, $reflection);
59
+                    break;
60
+            }
61
+        }
62
+        $data = new RecordService($db, $reflection);
63
+        $openApi = new OpenApiService($reflection);
64
+        foreach ($config->getControllers() as $controller) {
65
+            switch ($controller) {
66
+                case 'records':
67
+                    new RecordController($router, $responder, $data);
68
+                    break;
69
+                case 'columns':
70
+                    new ColumnController($router, $responder, $reflection, $definition);
71
+                    break;
72
+                case 'cache':
73
+                    new CacheController($router, $responder, $cache);
74
+                    break;
75
+                case 'openapi':
76
+                    new OpenApiController($router, $responder, $openApi);
77
+                    break;
78
+            }
79
+        }
80
+        $this->router = $router;
81
+        $this->responder = $responder;
82
+        $this->debug = $config->getDebug();
83
+    }
84
+
85
+    public function handle(Request $request): Response
86
+    {
87
+        $response = null;
88
+        try {
89
+            $response = $this->router->route($request);
90
+        } catch (\Throwable $e) {
91
+            if ($e instanceof \PDOException) {
92
+                if (strpos(strtolower($e->getMessage()), 'duplicate') !== false) {
93
+                    return $this->responder->error(ErrorCode::DUPLICATE_KEY_EXCEPTION, '');
94
+                }
95
+                if (strpos(strtolower($e->getMessage()), 'default value') !== false) {
96
+                    return $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
97
+                }
98
+                if (strpos(strtolower($e->getMessage()), 'allow nulls') !== false) {
99
+                    return $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
100
+                }
101
+                if (strpos(strtolower($e->getMessage()), 'constraint') !== false) {
102
+                    return $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
103
+                }
104
+            }
105
+            $response = $this->responder->error(ErrorCode::ERROR_NOT_FOUND, $e->getMessage());
106
+            if ($this->debug) {
107
+                $response->addHeader('X-Debug-Info', 'Exception in ' . $e->getFile() . ' on line ' . $e->getLine());
108
+            }
109
+        }
110
+        return $response;
111
+    }
112
+}

+ 9
- 0
src/Tqdev/PhpCrudApi/Cache/Cache.php View File

@@ -0,0 +1,9 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Cache;
3
+
4
+interface Cache
5
+{
6
+    public function set(String $key, String $value, int $ttl = 0): bool;
7
+    public function get(String $key): String;
8
+    public function clear(): bool;
9
+}

+ 35
- 0
src/Tqdev/PhpCrudApi/Cache/CacheFactory.php View File

@@ -0,0 +1,35 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Cache;
3
+
4
+use Tqdev\PhpCrudApi\Config;
5
+
6
+class CacheFactory
7
+{
8
+    const PREFIX = 'phpcrudapi-%s-';
9
+
10
+    private static function getPrefix(): String
11
+    {
12
+        return sprintf(self::PREFIX, substr(md5(__FILE__), 0, 8));
13
+    }
14
+
15
+    public static function create(Config $config): Cache
16
+    {
17
+        switch ($config->getCacheType()) {
18
+            case 'TempFile':
19
+                $cache = new TempFileCache(self::getPrefix(), $config->getCachePath());
20
+                break;
21
+            case 'Redis':
22
+                $cache = new RedisCache(self::getPrefix(), $config->getCachePath());
23
+                break;
24
+            case 'Memcache':
25
+                $cache = new MemcacheCache(self::getPrefix(), $config->getCachePath());
26
+                break;
27
+            case 'Memcached':
28
+                $cache = new MemcachedCache(self::getPrefix(), $config->getCachePath());
29
+                break;
30
+            default:
31
+                $cache = new NoCache();
32
+        }
33
+        return $cache;
34
+    }
35
+}

+ 44
- 0
src/Tqdev/PhpCrudApi/Cache/MemcacheCache.php View File

@@ -0,0 +1,44 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Cache;
3
+
4
+class MemcacheCache implements Cache
5
+{
6
+    protected $prefix;
7
+    protected $memcache;
8
+
9
+    public function __construct(String $prefix, String $config)
10
+    {
11
+        $this->prefix = $prefix;
12
+        if ($config == '') {
13
+            $address = 'localhost';
14
+            $port = 11211;
15
+        } elseif (strpos($config, ':') === false) {
16
+            $address = $config;
17
+            $port = 11211;
18
+        } else {
19
+            list($address, $port) = explode(':', $config);
20
+        }
21
+        $this->memcache = $this->create();
22
+        $this->memcache->addServer($address, $port);
23
+    }
24
+
25
+    protected function create(): object
26
+    {
27
+        return new \Memcache();
28
+    }
29
+
30
+    public function set(String $key, String $value, int $ttl = 0): bool
31
+    {
32
+        return $this->memcache->set($this->prefix . $key, $value, 0, $ttl);
33
+    }
34
+
35
+    public function get(String $key): String
36
+    {
37
+        return $this->memcache->get($this->prefix . $key) ?: '';
38
+    }
39
+
40
+    public function clear(): bool
41
+    {
42
+        return $this->memcache->flush();
43
+    }
44
+}

+ 15
- 0
src/Tqdev/PhpCrudApi/Cache/MemcachedCache.php View File

@@ -0,0 +1,15 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Cache;
3
+
4
+class MemcachedCache extends MemcacheCache
5
+{
6
+    protected function create(): object
7
+    {
8
+        return new \Memcached();
9
+    }
10
+
11
+    public function set(String $key, String $value, int $ttl = 0): bool
12
+    {
13
+        return $this->memcache->set($this->prefix . $key, $value, $ttl);
14
+    }
15
+}

+ 24
- 0
src/Tqdev/PhpCrudApi/Cache/NoCache.php View File

@@ -0,0 +1,24 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Cache;
3
+
4
+class NoCache implements Cache
5
+{
6
+    public function __construct()
7
+    {
8
+    }
9
+
10
+    public function set(String $key, String $value, int $ttl = 0): bool
11
+    {
12
+        return true;
13
+    }
14
+
15
+    public function get(String $key): String
16
+    {
17
+        return '';
18
+    }
19
+
20
+    public function clear(): bool
21
+    {
22
+        return true;
23
+    }
24
+}

+ 37
- 0
src/Tqdev/PhpCrudApi/Cache/RedisCache.php View File

@@ -0,0 +1,37 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Cache;
3
+
4
+class RedisCache implements Cache
5
+{
6
+    protected $prefix;
7
+    protected $redis;
8
+
9
+    public function __construct(String $prefix, String $config)
10
+    {
11
+        $this->prefix = $prefix;
12
+        if ($config == '') {
13
+            $config = '127.0.0.1';
14
+        }
15
+        $params = explode(':', $config, 6);
16
+        if (isset($params[3])) {
17
+            $params[3] = null;
18
+        }
19
+        $this->redis = new \Redis();
20
+        call_user_func_array(array($this->redis, 'pconnect'), $params);
21
+    }
22
+
23
+    public function set(String $key, String $value, int $ttl = 0): bool
24
+    {
25
+        return $this->redis->set($this->prefix . $key, $value, $ttl);
26
+    }
27
+
28
+    public function get(String $key): String
29
+    {
30
+        return $this->redis->get($this->prefix . $key) ?: '';
31
+    }
32
+
33
+    public function clear(): bool
34
+    {
35
+        return $this->redis->flushDb();
36
+    }
37
+}

+ 121
- 0
src/Tqdev/PhpCrudApi/Cache/TempFileCache.php View File

@@ -0,0 +1,121 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Cache;
3
+
4
+class TempFileCache implements Cache
5
+{
6
+    const SUFFIX = 'cache';
7
+
8
+    private $path;
9
+    private $segments;
10
+
11
+    public function __construct(String $prefix, String $config)
12
+    {
13
+        $this->segments = [];
14
+        $s = DIRECTORY_SEPARATOR;
15
+        $ps = PATH_SEPARATOR;
16
+        if ($config == '') {
17
+            $id = substr(md5(__FILE__), 0, 8);
18
+            $this->path = sys_get_temp_dir() . $s . $prefix . self::SUFFIX;
19
+        } elseif (strpos($config, $ps) === false) {
20
+            $this->path = $config;
21
+        } else {
22
+            list($path, $segments) = explode($ps, $config);
23
+            $this->path = $path;
24
+            $this->segments = explode(',', $segments);
25
+        }
26
+        if (file_exists($this->path) && is_dir($this->path)) {
27
+            $this->clean($this->path, array_filter($this->segments), strlen(md5('')), false);
28
+        }
29
+    }
30
+
31
+    private function getFileName(String $key): String
32
+    {
33
+        $s = DIRECTORY_SEPARATOR;
34
+        $md5 = md5($key);
35
+        $filename = rtrim($this->path, $s) . $s;
36
+        $i = 0;
37
+        foreach ($this->segments as $segment) {
38
+            $filename .= substr($md5, $i, $segment) . $s;
39
+            $i += $segment;
40
+        }
41
+        $filename .= substr($md5, $i);
42
+        return $filename;
43
+    }
44
+
45
+    public function set(String $key, String $value, int $ttl = 0): bool
46
+    {
47
+        $filename = $this->getFileName($key);
48
+        $dirname = dirname($filename);
49
+        if (!file_exists($dirname)) {
50
+            if (!mkdir($dirname, 0755, true)) {
51
+                return false;
52
+            }
53
+        }
54
+        $string = $ttl . '|' . $value;
55
+        return file_put_contents($filename, $string, LOCK_EX) !== false;
56
+    }
57
+
58
+    private function getString($filename): String
59
+    {
60
+        $data = file_get_contents($filename);
61
+        if ($data === false) {
62
+            return '';
63
+        }
64
+        list($ttl, $string) = explode('|', $data, 2);
65
+        if ($ttl > 0 && time() - filemtime($filename) > $ttl) {
66
+            return '';
67
+        }
68
+        return $string;
69
+    }
70
+
71
+    public function get(String $key): String
72
+    {
73
+        $filename = $this->getFileName($key);
74
+        if (!file_exists($filename)) {
75
+            return '';
76
+        }
77
+        $string = $this->getString($filename);
78
+        if ($string == null) {
79
+            return '';
80
+        }
81
+        return $string;
82
+    }
83
+
84
+    private function clean(String $path, array $segments, int $len, bool $all)/*: void*/
85
+    {
86
+        $entries = scandir($path);
87
+        foreach ($entries as $entry) {
88
+            if ($entry === '.' || $entry === '..') {
89
+                continue;
90
+            }
91
+            $filename = $path . DIRECTORY_SEPARATOR . $entry;
92
+            if (count($segments) == 0) {
93
+                if (strlen($entry) != $len) {
94
+                    continue;
95
+                }
96
+                if (is_file($filename)) {
97
+                    if ($all || $this->getString($filename) == null) {
98
+                        unlink($filename);
99
+                    }
100
+                }
101
+            } else {
102
+                if (strlen($entry) != $segments[0]) {
103
+                    continue;
104
+                }
105
+                if (is_dir($filename)) {
106
+                    $this->clean($filename, array_slice($segments, 1), $len - $segments[0], $all);
107
+                    rmdir($filename);
108
+                }
109
+            }
110
+        }
111
+    }
112
+
113
+    public function clear(): bool
114
+    {
115
+        if (!file_exists($this->path) || !is_dir($this->path)) {
116
+            return false;
117
+        }
118
+        $this->clean($this->path, array_filter($this->segments), strlen(md5('')), true);
119
+        return true;
120
+    }
121
+}

+ 158
- 0
src/Tqdev/PhpCrudApi/Column/DefinitionService.php View File

@@ -0,0 +1,158 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Column;
3
+
4
+use Tqdev\PhpCrudApi\Database\GenericDB;
5
+use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
6
+use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
7
+
8
+class DefinitionService
9
+{
10
+    private $db;
11
+    private $reflection;
12
+
13
+    public function __construct(GenericDB $db, ReflectionService $reflection)
14
+    {
15
+        $this->db = $db;
16
+        $this->reflection = $reflection;
17
+    }
18
+
19
+    public function updateTable(String $tableName, /* object */ $changes): bool
20
+    {
21
+        $table = $this->reflection->getTable($tableName);
22
+        $newTable = ReflectedTable::fromJson((object) array_merge((array) $table->jsonSerialize(), (array) $changes));
23
+        if ($table->getName() != $newTable->getName()) {
24
+            if (!$this->db->definition()->renameTable($table->getName(), $newTable->getName())) {
25
+                return false;
26
+            }
27
+        }
28
+        return true;
29
+    }
30
+
31
+    public function updateColumn(String $tableName, String $columnName, /* object */ $changes): bool
32
+    {
33
+        $table = $this->reflection->getTable($tableName);
34
+        $column = $table->get($columnName);
35
+
36
+        // remove constraints on other column
37
+        $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes));
38
+        if ($newColumn->getPk() != $column->getPk() && $table->hasPk()) {
39
+            $oldColumn = $table->getPk();
40
+            if ($oldColumn->getName() != $columnName) {
41
+                $oldColumn->setPk(false);
42
+                if (!$this->db->definition()->removeColumnPrimaryKey($table->getName(), $oldColumn->getName(), $oldColumn)) {
43
+                    return false;
44
+                }
45
+            }
46
+        }
47
+
48
+        // remove constraints
49
+        $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), ['pk' => false, 'fk' => false]));
50
+        if ($newColumn->getPk() != $column->getPk() && !$newColumn->getPk()) {
51
+            if (!$this->db->definition()->removeColumnPrimaryKey($table->getName(), $column->getName(), $newColumn)) {
52
+                return false;
53
+            }
54
+        }
55
+        if ($newColumn->getFk() != $column->getFk() && !$newColumn->getFk()) {
56
+            if (!$this->db->definition()->removeColumnForeignKey($table->getName(), $column->getName(), $newColumn)) {
57
+                return false;
58
+            }
59
+        }
60
+
61
+        // name and type
62
+        $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes));
63
+        $newColumn->setPk(false);
64
+        $newColumn->setFk('');
65
+        if ($newColumn->getName() != $column->getName()) {
66
+            if (!$this->db->definition()->renameColumn($table->getName(), $column->getName(), $newColumn)) {
67
+                return false;
68
+            }
69
+        }
70
+        if ($newColumn->getType() != $column->getType() ||
71
+            $newColumn->getLength() != $column->getLength() ||
72
+            $newColumn->getPrecision() != $column->getPrecision() ||
73
+            $newColumn->getScale() != $column->getScale()
74
+        ) {
75
+            if (!$this->db->definition()->retypeColumn($table->getName(), $newColumn->getName(), $newColumn)) {
76
+                return false;
77
+            }
78
+        }
79
+        if ($newColumn->getNullable() != $column->getNullable()) {
80
+            if (!$this->db->definition()->setColumnNullable($table->getName(), $newColumn->getName(), $newColumn)) {
81
+                return false;
82
+            }
83
+        }
84
+
85
+        // add constraints
86
+        $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes));
87
+        if ($newColumn->getFk()) {
88
+            if (!$this->db->definition()->addColumnForeignKey($table->getName(), $newColumn->getName(), $newColumn)) {
89
+                return false;
90
+            }
91
+        }
92
+        if ($newColumn->getPk()) {
93
+            if (!$this->db->definition()->addColumnPrimaryKey($table->getName(), $newColumn->getName(), $newColumn)) {
94
+                return false;
95
+            }
96
+        }
97
+        return true;
98
+    }
99
+
100
+    public function addTable( /* object */$definition)
101
+    {
102
+        $newTable = ReflectedTable::fromJson($definition);
103
+        if (!$this->db->definition()->addTable($newTable)) {
104
+            return false;
105
+        }
106
+        return true;
107
+    }
108
+
109
+    public function addColumn(String $tableName, /* object */ $definition)
110
+    {
111
+        $newColumn = ReflectedColumn::fromJson($definition);
112
+        if (!$this->db->definition()->addColumn($tableName, $newColumn)) {
113
+            return false;
114
+        }
115
+        if ($newColumn->getFk()) {
116
+            if (!$this->db->definition()->addColumnForeignKey($tableName, $newColumn->getName(), $newColumn)) {
117
+                return false;
118
+            }
119
+        }
120
+        if ($newColumn->getPk()) {
121
+            if (!$this->db->definition()->addColumnPrimaryKey($tableName, $newColumn->getName(), $newColumn)) {
122
+                return false;
123
+            }
124
+        }
125
+        return true;
126
+    }
127
+
128
+    public function removeTable(String $tableName)
129
+    {
130
+        if (!$this->db->definition()->removeTable($tableName)) {
131
+            return false;
132
+        }
133
+        return true;
134
+    }
135
+
136
+    public function removeColumn(String $tableName, String $columnName)
137
+    {
138
+        $table = $this->reflection->getTable($tableName);
139
+        $newColumn = $table->get($columnName);
140
+        if ($newColumn->getPk()) {
141
+            $newColumn->setPk(false);
142
+            if (!$this->db->definition()->removeColumnPrimaryKey($table->getName(), $newColumn->getName(), $newColumn)) {
143
+                return false;
144
+            }
145
+        }
146
+        if ($newColumn->getFk()) {
147
+            $newColumn->setFk("");
148
+            if (!$this->db->definition()->removeColumnForeignKey($tableName, $columnName, $newColumn)) {
149
+                return false;
150
+            }
151
+        }
152
+        if (!$this->db->definition()->removeColumn($tableName, $columnName)) {
153
+            return false;
154
+        }
155
+        return true;
156
+    }
157
+
158
+}

+ 165
- 0
src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedColumn.php View File

@@ -0,0 +1,165 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Column\Reflection;
3
+
4
+use Tqdev\PhpCrudApi\Database\GenericReflection;
5
+
6
+class ReflectedColumn implements \JsonSerializable
7
+{
8
+    const DEFAULT_LENGTH = 255;
9
+    const DEFAULT_PRECISION = 19;
10
+    const DEFAULT_SCALE = 4;
11
+
12
+    private $name;
13
+    private $type;
14
+    private $length;
15
+    private $precision;
16
+    private $scale;
17
+    private $nullable;
18
+    private $pk;
19
+    private $fk;
20
+
21
+    public function __construct(String $name, String $type, int $length, int $precision, int $scale, bool $nullable, bool $pk, String $fk)
22
+    {
23
+        $this->name = $name;
24
+        $this->type = $type;
25
+        $this->length = $length;
26
+        $this->precision = $precision;
27
+        $this->scale = $scale;
28
+        $this->nullable = $nullable;
29
+        $this->pk = $pk;
30
+        $this->fk = $fk;
31
+        $this->sanitize();
32
+    }
33
+
34
+    public static function fromReflection(GenericReflection $reflection, array $columnResult): ReflectedColumn
35
+    {
36
+        $name = $columnResult['COLUMN_NAME'];
37
+        $length = $columnResult['CHARACTER_MAXIMUM_LENGTH'] + 0;
38
+        $type = $reflection->toJdbcType($columnResult['DATA_TYPE'], $length);
39
+        $precision = $columnResult['NUMERIC_PRECISION'] + 0;
40
+        $scale = $columnResult['NUMERIC_SCALE'] + 0;
41
+        $nullable = in_array(strtoupper($columnResult['IS_NULLABLE']), ['TRUE', 'YES', 'T', 'Y', '1']);
42
+        $pk = false;
43
+        $fk = '';
44
+        return new ReflectedColumn($name, $type, $length, $precision, $scale, $nullable, $pk, $fk);
45
+    }
46
+
47
+    public static function fromJson( /* object */$json): ReflectedColumn
48
+    {
49
+        $name = $json->name;
50
+        $type = $json->type;
51
+        $length = isset($json->length) ? $json->length : 0;
52
+        $precision = isset($json->precision) ? $json->precision : 0;
53
+        $scale = isset($json->scale) ? $json->scale : 0;
54
+        $nullable = isset($json->nullable) ? $json->nullable : false;
55
+        $pk = isset($json->pk) ? $json->pk : false;
56
+        $fk = isset($json->fk) ? $json->fk : '';
57
+        return new ReflectedColumn($name, $type, $length, $precision, $scale, $nullable, $pk, $fk);
58
+    }
59
+
60
+    private function sanitize()
61
+    {
62
+        $this->length = $this->hasLength() ? $this->getLength() : 0;
63
+        $this->precision = $this->hasPrecision() ? $this->getPrecision() : 0;
64
+        $this->scale = $this->hasScale() ? $this->getScale() : 0;
65
+    }
66
+
67
+    public function getName(): String
68
+    {
69
+        return $this->name;
70
+    }
71
+
72
+    public function getNullable(): bool
73
+    {
74
+        return $this->nullable;
75
+    }
76
+
77
+    public function getType(): String
78
+    {
79
+        return $this->type;
80
+    }
81
+
82
+    public function getLength(): int
83
+    {
84
+        return $this->length ?: self::DEFAULT_LENGTH;
85
+    }
86
+
87
+    public function getPrecision(): int
88
+    {
89
+        return $this->precision ?: self::DEFAULT_PRECISION;
90
+    }
91
+
92
+    public function getScale(): int
93
+    {
94
+        return $this->scale ?: self::DEFAULT_SCALE;
95
+    }
96
+
97
+    public function hasLength(): bool
98
+    {
99
+        return in_array($this->type, ['varchar', 'varbinary']);
100
+    }
101
+
102
+    public function hasPrecision(): bool
103
+    {
104
+        return $this->type == 'decimal';
105
+    }
106
+
107
+    public function hasScale(): bool
108
+    {
109
+        return $this->type == 'decimal';
110
+    }
111
+
112
+    public function isBinary(): bool
113
+    {
114
+        return in_array($this->type, ['blob', 'varbinary']);
115
+    }
116
+
117
+    public function isBoolean(): bool
118
+    {
119
+        return $this->type == 'boolean';
120
+    }
121
+
122
+    public function isGeometry(): bool
123
+    {
124
+        return $this->type == 'geometry';
125
+    }
126
+
127
+    public function setPk($value) /*: void*/
128
+    {
129
+        $this->pk = $value;
130
+    }
131
+
132
+    public function getPk(): bool
133
+    {
134
+        return $this->pk;
135
+    }
136
+
137
+    public function setFk($value) /*: void*/
138
+    {
139
+        $this->fk = $value;
140
+    }
141
+
142
+    public function getFk(): String
143
+    {
144
+        return $this->fk;
145
+    }
146
+
147
+    public function serialize()
148
+    {
149
+        return [
150
+            'name' => $this->name,
151
+            'type' => $this->type,
152
+            'length' => $this->length,
153
+            'precision' => $this->precision,
154
+            'scale' => $this->scale,
155
+            'nullable' => $this->nullable,
156
+            'pk' => $this->pk,
157
+            'fk' => $this->fk,
158
+        ];
159
+    }
160
+
161
+    public function jsonSerialize()
162
+    {
163
+        return array_filter($this->serialize());
164
+    }
165
+}

+ 78
- 0
src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedDatabase.php View File

@@ -0,0 +1,78 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Column\Reflection;
3
+
4
+use Tqdev\PhpCrudApi\Database\GenericReflection;
5
+
6
+class ReflectedDatabase implements \JsonSerializable
7
+{
8
+    private $name;
9
+    private $tables;
10
+
11
+    public function __construct(String $name, array $tables)
12
+    {
13
+        $this->name = $name;
14
+        $this->tables = [];
15
+        foreach ($tables as $table) {
16
+            $this->tables[$table->getName()] = $table;
17
+        }
18
+    }
19
+
20
+    public static function fromReflection(GenericReflection $reflection): ReflectedDatabase
21
+    {
22
+        $name = $reflection->getDatabaseName();
23
+        $tables = [];
24
+        foreach ($reflection->getTables() as $tableName) {
25
+            if (in_array($tableName['TABLE_NAME'], $reflection->getIgnoredTables())) {
26
+                continue;
27
+            }
28
+            $table = ReflectedTable::fromReflection($reflection, $tableName);
29
+            $tables[$table->getName()] = $table;
30
+        }
31
+        return new ReflectedDatabase($name, array_values($tables));
32
+    }
33
+
34
+    public static function fromJson( /* object */$json): ReflectedDatabase
35
+    {
36
+        $name = $json->name;
37
+        $tables = [];
38
+        if (isset($json->tables) && is_array($json->tables)) {
39
+            foreach ($json->tables as $table) {
40
+                $tables[] = ReflectedTable::fromJson($table);
41
+            }
42
+        }
43
+        return new ReflectedDatabase($name, $tables);
44
+    }
45
+
46
+    public function getName(): String
47
+    {
48
+        return $this->name;
49
+    }
50
+
51
+    public function exists(String $tableName): bool
52
+    {
53
+        return isset($this->tables[$tableName]);
54
+    }
55
+
56
+    public function get(String $tableName): ReflectedTable
57
+    {
58
+        return $this->tables[$tableName];
59
+    }
60
+
61
+    public function getTableNames(): array
62
+    {
63
+        return array_keys($this->tables);
64
+    }
65
+
66
+    public function serialize()
67
+    {
68
+        return [
69
+            'name' => $this->name,
70
+            'tables' => array_values($this->tables),
71
+        ];
72
+    }
73
+
74
+    public function jsonSerialize()
75
+    {
76
+        return $this->serialize();
77
+    }
78
+}

+ 131
- 0
src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedTable.php View File

@@ -0,0 +1,131 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Column\Reflection;
3
+
4
+use Tqdev\PhpCrudApi\Database\GenericReflection;
5
+
6
+class ReflectedTable implements \JsonSerializable
7
+{
8
+    private $name;
9
+    private $columns;
10
+    private $pk;
11
+    private $fks;
12
+
13
+    public function __construct(String $name, array $columns)
14
+    {
15
+        $this->name = $name;
16
+        // set columns
17
+        $this->columns = [];
18
+        foreach ($columns as $column) {
19
+            $columnName = $column->getName();
20
+            $this->columns[$columnName] = $column;
21
+        }
22
+        // set primary key
23
+        $this->pk = null;
24
+        foreach ($columns as $column) {
25
+            if ($column->getPk() == true) {
26
+                $this->pk = $column;
27
+            }
28
+        }
29
+        // set foreign keys
30
+        $this->fks = [];
31
+        foreach ($columns as $column) {
32
+            $columnName = $column->getName();
33
+            $referencedTableName = $column->getFk();
34
+            if ($referencedTableName != '') {
35
+                $this->fks[$columnName] = $referencedTableName;
36
+            }
37
+        }
38
+    }
39
+
40
+    public static function fromReflection(GenericReflection $reflection, array $tableResult): ReflectedTable
41
+    {
42
+        $name = $tableResult['TABLE_NAME'];
43
+        // set columns
44
+        $columns = [];
45
+        foreach ($reflection->getTableColumns($name) as $tableColumn) {
46
+            $column = ReflectedColumn::fromReflection($reflection, $tableColumn);
47
+            $columns[$column->getName()] = $column;
48
+        }
49
+        // set primary key
50
+        $columnNames = $reflection->getTablePrimaryKeys($name);
51
+        if (count($columnNames) == 1) {
52
+            $columnName = $columnNames[0];
53
+            if (isset($columns[$columnName])) {
54
+                $pk = $columns[$columnName];
55
+                $pk->setPk(true);
56
+            }
57
+        }
58
+        // set foreign keys
59
+        $fks = $reflection->getTableForeignKeys($name);
60
+        foreach ($fks as $columnName => $table) {
61
+            $columns[$columnName]->setFk($table);
62
+        }
63
+        return new ReflectedTable($name, array_values($columns));
64
+    }
65
+
66
+    public static function fromJson( /* object */$json): ReflectedTable
67
+    {
68
+        $name = $json->name;
69
+        $columns = [];
70
+        if (isset($json->columns) && is_array($json->columns)) {
71
+            foreach ($json->columns as $column) {
72
+                $columns[] = ReflectedColumn::fromJson($column);
73
+            }
74
+        }
75
+        return new ReflectedTable($name, $columns);
76
+    }
77
+
78
+    public function exists(String $columnName): bool
79
+    {
80
+        return isset($this->columns[$columnName]);
81
+    }
82
+
83
+    public function hasPk(): bool
84
+    {
85
+        return $this->pk != null;
86
+    }
87
+
88
+    public function getPk(): ReflectedColumn
89
+    {
90
+        return $this->pk;
91
+    }
92
+
93
+    public function getName(): String
94
+    {
95
+        return $this->name;
96
+    }
97
+
98
+    public function columnNames(): array
99
+    {
100
+        return array_keys($this->columns);
101
+    }
102
+
103
+    public function get($columnName): ReflectedColumn
104
+    {
105
+        return $this->columns[$columnName];
106
+    }
107
+
108
+    public function getFksTo(String $tableName): array
109
+    {
110
+        $columns = array();
111
+        foreach ($this->fks as $columnName => $referencedTableName) {
112
+            if ($tableName == $referencedTableName) {
113
+                $columns[] = $this->columns[$columnName];
114
+            }
115
+        }
116
+        return $columns;
117
+    }
118
+
119
+    public function serialize()
120
+    {
121
+        return [
122
+            'name' => $this->name,
123
+            'columns' => array_values($this->columns),
124
+        ];
125
+    }
126
+
127
+    public function jsonSerialize()
128
+    {
129
+        return $this->serialize();
130
+    }
131
+}

+ 56
- 0
src/Tqdev/PhpCrudApi/Column/ReflectionService.php View File

@@ -0,0 +1,56 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Column;
3
+
4
+use Tqdev\PhpCrudApi\Cache\Cache;
5
+use Tqdev\PhpCrudApi\Column\Reflection\ReflectedDatabase;
6
+use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
7
+use Tqdev\PhpCrudApi\Database\GenericDB;
8
+
9
+class ReflectionService
10
+{
11
+    private $db;
12
+    private $cache;
13
+    private $ttl;
14
+    private $tables;
15
+
16
+    public function __construct(GenericDB $db, Cache $cache, int $ttl)
17
+    {
18
+        $this->db = $db;
19
+        $this->cache = $cache;
20
+        $this->ttl = $ttl;
21
+        $this->tables = $this->loadTables(true);
22
+    }
23
+
24
+    private function loadTables(bool $useCache): ReflectedDatabase
25
+    {
26
+        $data = $useCache ? $this->cache->get('ReflectedDatabase') : '';
27
+        if ($data != '') {
28
+            $tables = ReflectedDatabase::fromJson(json_decode(gzuncompress($data)));
29
+        } else {
30
+            $tables = ReflectedDatabase::fromReflection($this->db->reflection());
31
+            $data = gzcompress(json_encode($tables, JSON_UNESCAPED_UNICODE));
32
+            $this->cache->set('ReflectedDatabase', $data, $this->ttl);
33
+        }
34
+        return $tables;
35
+    }
36
+
37
+    public function refresh()
38
+    {
39
+        $this->tables = $this->loadTables(false);
40
+    }
41
+
42
+    public function hasTable(String $table): bool
43
+    {
44
+        return $this->tables->exists($table);
45
+    }
46
+
47
+    public function getTable(String $table): ReflectedTable
48
+    {
49
+        return $this->tables->get($table);
50
+    }
51
+
52
+    public function getDatabase(): ReflectedDatabase
53
+    {
54
+        return $this->tables;
55
+    }
56
+}

+ 153
- 0
src/Tqdev/PhpCrudApi/Config.php View File

@@ -0,0 +1,153 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi;
3
+
4
+class Config
5
+{
6
+    private $values = [
7
+        'driver' => null,
8
+        'address' => 'localhost',
9
+        'port' => null,
10
+        'username' => null,
11
+        'password' => null,
12
+        'database' => null,
13
+        'middlewares' => 'cors',
14
+        'controllers' => 'records,columns,cache,openapi',
15
+        'cacheType' => 'TempFile',
16
+        'cachePath' => '',
17
+        'cacheTime' => 10,
18
+        'debug' => false,
19
+    ];
20
+
21
+    private function getDefaultDriver(array $values): String
22
+    {
23
+        if (isset($values['driver'])) {
24
+            return $values['driver'];
25
+        }
26
+        return 'mysql';
27
+    }
28
+
29
+    private function getDefaultPort(String $driver): int
30
+    {
31
+        switch ($driver) {
32
+            case 'mysql':return 3306;
33
+            case 'pgsql':return 5432;
34
+            case 'sqlsrv':return 1433;
35
+        }
36
+    }
37
+
38
+    private function getDefaultAddress(String $driver): String
39
+    {
40
+        switch ($driver) {
41
+            case 'mysql':return 'localhost';
42
+            case 'pgsql':return 'localhost';
43
+            case 'sqlsrv':return 'localhost';
44
+        }
45
+    }
46
+
47
+    private function getDriverDefaults(String $driver): array
48
+    {
49
+        return [
50
+            'driver' => $driver,
51
+            'address' => $this->getDefaultAddress($driver),
52
+            'port' => $this->getDefaultPort($driver),
53
+        ];
54
+    }
55
+
56
+    public function __construct(array $values)
57
+    {
58
+        $driver = $this->getDefaultDriver($values);
59
+        $defaults = $this->getDriverDefaults($driver);
60
+        $newValues = array_merge($this->values, $defaults, $values);
61
+        $newValues = $this->parseMiddlewares($newValues);
62
+        $diff = array_diff_key($newValues, $this->values);
63
+        if (!empty($diff)) {
64
+            $key = array_keys($diff)[0];
65
+            throw new \Exception("Config has invalid value '$key'");
66
+        }
67
+        $this->values = $newValues;
68
+    }
69
+
70
+    private function parseMiddlewares(array $values): array
71
+    {
72
+        $newValues = array();
73
+        $properties = array();
74
+        $middlewares = array_map('trim', explode(',', $values['middlewares']));
75
+        foreach ($middlewares as $middleware) {
76
+            $properties[$middleware] = [];
77
+        }
78
+        foreach ($values as $key => $value) {
79
+            if (strpos($key, '.') === false) {
80
+                $newValues[$key] = $value;
81
+            } else {
82
+                list($middleware, $key2) = explode('.', $key, 2);
83
+                if (isset($properties[$middleware])) {
84
+                    $properties[$middleware][$key2] = $value;
85
+                } else {
86
+                    throw new \Exception("Config has invalid value '$key'");
87
+                }
88
+            }
89
+        }
90
+        $newValues['middlewares'] = $properties;
91
+        return $newValues;
92
+    }
93
+
94
+    public function getDriver(): String
95
+    {
96
+        return $this->values['driver'];
97
+    }
98
+
99
+    public function getAddress(): String
100
+    {
101
+        return $this->values['address'];
102
+    }
103
+
104
+    public function getPort(): int
105
+    {
106
+        return $this->values['port'];
107
+    }
108
+
109
+    public function getUsername(): String
110
+    {
111
+        return $this->values['username'];
112
+    }
113
+
114
+    public function getPassword(): String
115
+    {
116
+        return $this->values['password'];
117
+    }
118
+
119
+    public function getDatabase(): String
120
+    {
121
+        return $this->values['database'];
122
+    }
123
+
124
+    public function getMiddlewares(): array
125
+    {
126
+        return $this->values['middlewares'];
127
+    }
128
+
129
+    public function getControllers(): array
130
+    {
131
+        return array_map('trim', explode(',', $this->values['controllers']));
132
+    }
133
+
134
+    public function getCacheType(): String
135
+    {
136
+        return $this->values['cacheType'];
137
+    }
138
+
139
+    public function getCachePath(): String
140
+    {
141
+        return $this->values['cachePath'];
142
+    }
143
+
144
+    public function getCacheTime(): int
145
+    {
146
+        return $this->values['cacheTime'];
147
+    }
148
+
149
+    public function getDebug(): String
150
+    {
151
+        return $this->values['debug'];
152
+    }
153
+}

+ 26
- 0
src/Tqdev/PhpCrudApi/Controller/CacheController.php View File

@@ -0,0 +1,26 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Controller;
3
+
4
+use Tqdev\PhpCrudApi\Cache\Cache;
5
+use Tqdev\PhpCrudApi\Request;
6
+use Tqdev\PhpCrudApi\Response;
7
+use Tqdev\PhpCrudApi\Middleware\Router\Router;
8
+
9
+class CacheController
10
+{
11
+    private $cache;
12
+    private $responder;
13
+
14
+    public function __construct(Router $router, Responder $responder, Cache $cache)
15
+    {
16
+        $router->register('GET', '/cache/clear', array($this, 'clear'));
17
+        $this->cache = $cache;
18
+        $this->responder = $responder;
19
+    }
20
+
21
+    public function clear(Request $request): Response
22
+    {
23
+        return $this->responder->success($this->cache->clear());
24
+    }
25
+
26
+}

+ 156
- 0
src/Tqdev/PhpCrudApi/Controller/ColumnController.php View File

@@ -0,0 +1,156 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Controller;
3
+
4
+use Tqdev\PhpCrudApi\Record\ErrorCode;
5
+use Tqdev\PhpCrudApi\Column\DefinitionService;
6
+use Tqdev\PhpCrudApi\Column\ReflectionService;
7
+use Tqdev\PhpCrudApi\Request;
8
+use Tqdev\PhpCrudApi\Response;
9
+use Tqdev\PhpCrudApi\Middleware\Router\Router;
10
+
11
+class ColumnController
12
+{
13
+    private $responder;
14
+    private $reflection;
15
+    private $definition;
16
+
17
+    public function __construct(Router $router, Responder $responder, ReflectionService $reflection, DefinitionService $definition)
18
+    {
19
+        $router->register('GET', '/columns', array($this, 'getDatabase'));
20
+        $router->register('GET', '/columns/*', array($this, 'getTable'));
21
+        $router->register('GET', '/columns/*/*', array($this, 'getColumn'));
22
+        $router->register('PUT', '/columns/*', array($this, 'updateTable'));
23
+        $router->register('PUT', '/columns/*/*', array($this, 'updateColumn'));
24
+        $router->register('POST', '/columns', array($this, 'addTable'));
25
+        $router->register('POST', '/columns/*', array($this, 'addColumn'));
26
+        $router->register('DELETE', '/columns/*', array($this, 'removeTable'));
27
+        $router->register('DELETE', '/columns/*/*', array($this, 'removeColumn'));
28
+        $this->responder = $responder;
29
+        $this->reflection = $reflection;
30
+        $this->definition = $definition;
31
+    }
32
+
33
+    public function getDatabase(Request $request): Response
34
+    {
35
+        $database = $this->reflection->getDatabase();
36
+        return $this->responder->success($database);
37
+    }
38
+
39
+    public function getTable(Request $request): Response
40
+    {
41
+        $tableName = $request->getPathSegment(2);
42
+        if (!$this->reflection->hasTable($tableName)) {
43
+            return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
44
+        }
45
+        $table = $this->reflection->getTable($tableName);
46
+        return $this->responder->success($table);
47
+    }
48
+
49
+    public function getColumn(Request $request): Response
50
+    {
51
+        $tableName = $request->getPathSegment(2);
52
+        $columnName = $request->getPathSegment(3);
53
+        if (!$this->reflection->hasTable($tableName)) {
54
+            return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
55
+        }
56
+        $table = $this->reflection->getTable($tableName);
57
+        if (!$table->exists($columnName)) {
58
+            return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
59
+        }
60
+        $column = $table->get($columnName);
61
+        return $this->responder->success($column);
62
+    }
63
+
64
+    public function updateTable(Request $request): Response
65
+    {
66
+        $tableName = $request->getPathSegment(2);
67
+        if (!$this->reflection->hasTable($tableName)) {
68
+            return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
69
+        }
70
+        $success = $this->definition->updateTable($tableName, $request->getBody());
71
+        if ($success) {
72
+            $this->reflection->refresh();
73
+        }
74
+        return $this->responder->success($success);
75
+    }
76
+
77
+    public function updateColumn(Request $request): Response
78
+    {
79
+        $tableName = $request->getPathSegment(2);
80
+        $columnName = $request->getPathSegment(3);
81
+        if (!$this->reflection->hasTable($tableName)) {
82
+            return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
83
+        }
84
+        $table = $this->reflection->getTable($tableName);
85
+        if (!$table->exists($columnName)) {
86
+            return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
87
+        }
88
+        $success = $this->definition->updateColumn($tableName, $columnName, $request->getBody());
89
+        if ($success) {
90
+            $this->reflection->refresh();
91
+        }
92
+        return $this->responder->success($success);
93
+    }
94
+
95
+    public function addTable(Request $request): Response
96
+    {
97
+        $tableName = $request->getBody()->name;
98
+        if ($this->reflection->hasTable($tableName)) {
99
+            return $this->responder->error(ErrorCode::TABLE_ALREADY_EXISTS, $tableName);
100
+        }
101
+        $success = $this->definition->addTable($request->getBody());
102
+        if ($success) {
103
+            $this->reflection->refresh();
104
+        }
105
+        return $this->responder->success($success);
106
+    }
107
+
108
+    public function addColumn(Request $request): Response
109
+    {
110
+        $tableName = $request->getPathSegment(2);
111
+        if (!$this->reflection->hasTable($tableName)) {
112
+            return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
113
+        }
114
+        $columnName = $request->getBody()->name;
115
+        $table = $this->reflection->getTable($tableName);
116
+        if ($table->exists($columnName)) {
117
+            return $this->responder->error(ErrorCode::COLUMN_ALREADY_EXISTS, $columnName);
118
+        }
119
+        $success = $this->definition->addColumn($tableName, $request->getBody());
120
+        if ($success) {
121
+            $this->reflection->refresh();
122
+        }
123
+        return $this->responder->success($success);
124
+    }
125
+
126
+    public function removeTable(Request $request): Response
127
+    {
128
+        $tableName = $request->getPathSegment(2);
129
+        if (!$this->reflection->hasTable($tableName)) {
130
+            return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
131
+        }
132
+        $success = $this->definition->removeTable($tableName);
133
+        if ($success) {
134
+            $this->reflection->refresh();
135
+        }
136
+        return $this->responder->success($success);
137
+    }
138
+
139
+    public function removeColumn(Request $request): Response
140
+    {
141
+        $tableName = $request->getPathSegment(2);
142
+        $columnName = $request->getPathSegment(3);
143
+        if (!$this->reflection->hasTable($tableName)) {
144
+            return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
145
+        }
146
+        $table = $this->reflection->getTable($tableName);
147
+        if (!$table->exists($columnName)) {
148
+            return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
149
+        }
150
+        $success = $this->definition->removeColumn($tableName, $columnName);
151
+        if ($success) {
152
+            $this->reflection->refresh();
153
+        }
154
+        return $this->responder->success($success);
155
+    }
156
+}

+ 26
- 0
src/Tqdev/PhpCrudApi/Controller/OpenApiController.php View File

@@ -0,0 +1,26 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Controller;
3
+
4
+use Tqdev\PhpCrudApi\OpenApi\OpenApiService;
5
+use Tqdev\PhpCrudApi\Request;
6
+use Tqdev\PhpCrudApi\Response;
7
+use Tqdev\PhpCrudApi\Middleware\Router\Router;
8
+
9
+class OpenApiController
10
+{
11
+    private $openApi;
12
+    private $responder;
13
+
14
+    public function __construct(Router $router, Responder $responder, OpenApiService $openApi)
15
+    {
16
+        $router->register('GET', '/openapi', array($this, 'openapi'));
17
+        $this->openApi = $openApi;
18
+        $this->responder = $responder;
19
+    }
20
+
21
+    public function openapi(Request $request): Response
22
+    {
23
+        return $this->responder->success(false);
24
+    }
25
+
26
+}

+ 133
- 0
src/Tqdev/PhpCrudApi/Controller/RecordController.php View File

@@ -0,0 +1,133 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Controller;
3
+
4
+use Tqdev\PhpCrudApi\Record\ErrorCode;
5
+use Tqdev\PhpCrudApi\Record\RecordService;
6
+use Tqdev\PhpCrudApi\Request;
7
+use Tqdev\PhpCrudApi\Response;
8
+use Tqdev\PhpCrudApi\Middleware\Router\Router;
9
+
10
+class RecordController
11
+{
12
+    private $service;
13
+    private $responder;
14
+
15
+    public function __construct(Router $router, Responder $responder, RecordService $service)
16
+    {
17
+        $router->register('GET', '/records/*', array($this, '_list'));
18
+        $router->register('POST', '/records/*', array($this, 'create'));
19
+        $router->register('GET', '/records/*/*', array($this, 'read'));
20
+        $router->register('PUT', '/records/*/*', array($this, 'update'));
21
+        $router->register('DELETE', '/records/*/*', array($this, 'delete'));
22
+        $router->register('PATCH', '/records/*/*', array($this, 'increment'));
23
+        $this->service = $service;
24
+        $this->responder = $responder;
25
+    }
26
+
27
+    public function _list(Request $request): Response
28
+    {
29
+        $table = $request->getPathSegment(2);
30
+        $params = $request->getParams();
31
+        if (!$this->service->exists($table)) {
32
+            return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
33
+        }
34
+        return $this->responder->success($this->service->_list($table, $params));
35
+    }
36
+
37
+    public function read(Request $request): Response
38
+    {
39
+        $table = $request->getPathSegment(2);
40
+        $id = $request->getPathSegment(3);
41
+        $params = $request->getParams();
42
+        if (!$this->service->exists($table)) {
43
+            return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
44
+        }
45
+        if (strpos($id, ',') !== false) {
46
+            $ids = explode(',', $id);
47
+            $result = [];
48
+            for ($i = 0; $i < count($ids); $i++) {
49
+                array_push($result, $this->service->read($table, $ids[$i], $params));
50
+            }
51
+            return $this->responder->success($result);
52
+        } else {
53
+            $response = $this->service->read($table, $id, $params);
54
+            if ($response === null) {
55
+                return $this->responder->error(ErrorCode::RECORD_NOT_FOUND, $id);
56
+            }
57
+            return $this->responder->success($response);
58
+        }
59
+    }
60
+
61
+    public function create(Request $request): Response
62
+    {
63
+        $table = $request->getPathSegment(2);
64
+        $record = $request->getBody();
65
+        if ($record === null) {
66
+            return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, '');
67
+        }
68
+        $params = $request->getParams();
69
+        if (!$this->service->exists($table)) {
70
+            return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
71
+        }
72
+        if (is_array($record)) {
73
+            $result = array();
74
+            foreach ($record as $r) {
75
+                $result[] = $this->service->create($table, $r, $params);
76
+            }
77
+            return $this->responder->success($result);
78
+        } else {
79
+            return $this->responder->success($this->service->create($table, $record, $params));
80
+        }
81
+    }
82
+
83
+    public function update(Request $request): Response
84
+    {
85
+        $table = $request->getPathSegment(2);
86
+        $id = $request->getPathSegment(3);
87
+        $record = $request->getBody();
88
+        if ($record === null) {
89
+            return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, '');
90
+        }
91
+        $params = $request->getParams();
92
+        if (!$this->service->exists($table)) {
93
+            return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
94
+        }
95
+        $ids = explode(',', $id);
96
+        if (is_array($record)) {
97
+            if (count($ids) != count($record)) {
98
+                return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
99
+            }
100
+            $result = array();
101
+            for ($i = 0; $i < count($ids); $i++) {
102
+                $result[] = $this->service->update($table, $ids[$i], $record[$i], $params);
103
+            }
104
+            return $this->responder->success($result);
105
+        } else {
106
+            if (count($ids) != 1) {
107
+                return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
108
+            }
109
+            return $this->responder->success($this->service->update($table, $id, $record, $params));
110
+        }
111
+    }
112
+
113
+    public function delete(Request $request): Response
114
+    {
115
+        $table = $request->getPathSegment(2);
116
+        $id = $request->getPathSegment(3);
117
+        $params = $request->getParams();
118
+        if (!$this->service->exists($table)) {
119
+            return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
120
+        }
121
+        $ids = explode(',', $id);
122
+        if (count($ids) > 1) {
123
+            $result = array();
124
+            for ($i = 0; $i < count($ids); $i++) {
125
+                $result[] = $this->service->delete($table, $ids[$i], $params);
126
+            }
127
+            return $this->responder->success($result);
128
+        } else {
129
+            return $this->responder->success($this->service->delete($table, $id, $params));
130
+        }
131
+    }
132
+
133
+}

+ 23
- 0
src/Tqdev/PhpCrudApi/Controller/Responder.php View File

@@ -0,0 +1,23 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Controller;
3
+
4
+use Tqdev\PhpCrudApi\Record\Document\ErrorDocument;
5
+use Tqdev\PhpCrudApi\Record\ErrorCode;
6
+use Tqdev\PhpCrudApi\Response;
7
+
8
+class Responder
9
+{
10
+    public function error(int $error, String $argument, $details = null): Response
11
+    {
12
+        $errorCode = new ErrorCode($error);
13
+        $status = $errorCode->getStatus();
14
+        $document = new ErrorDocument($errorCode, $argument, $details);
15
+        return new Response($status, $document);
16
+    }
17
+
18
+    public function success($result): Response
19
+    {
20
+        return new Response(Response::OK, $result);
21
+    }
22
+
23
+}

+ 64
- 0
src/Tqdev/PhpCrudApi/Database/ColumnConverter.php View File

@@ -0,0 +1,64 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Database;
3
+
4
+use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
5
+
6
+class ColumnConverter
7
+{
8
+    private $driver;
9
+
10
+    public function __construct(String $driver)
11
+    {
12
+        $this->driver = $driver;
13
+    }
14
+
15
+    public function convertColumnValue(ReflectedColumn $column): String
16
+    {
17
+        if ($column->isBinary()) {
18
+            switch ($this->driver) {
19
+                case 'mysql':
20
+                    return "FROM_BASE64(?)";
21
+                case 'pgsql':
22
+                    return "decode(?, 'base64')";
23
+                case 'sqlsrv':
24
+                    return "CONVERT(XML, ?).value('.','varbinary(max)')";
25
+            }
26
+        }
27
+        if ($column->isGeometry()) {
28
+            switch ($this->driver) {
29
+                case 'mysql':
30
+                case 'pgsql':
31
+                    return "ST_GeomFromText(?)";
32
+                case 'sqlsrv':
33
+                    return "geometry::STGeomFromText(?,0)";
34
+            }
35
+        }
36
+        return '?';
37
+    }
38
+
39
+    public function convertColumnName(ReflectedColumn $column, $value): String
40
+    {
41
+        if ($column->isBinary()) {
42
+            switch ($this->driver) {
43
+                case 'mysql':
44
+                    return "TO_BASE64($value) as $value";
45
+                case 'pgsql':
46
+                    return "encode($value::bytea, 'base64') as $value";
47
+                case 'sqlsrv':
48
+                    return "CAST(N'' AS XML).value('xs:base64Binary(xs:hexBinary(sql:column($value)))', 'VARCHAR(MAX)') as $value";
49
+
50
+            }
51
+        }
52
+        if ($column->isGeometry()) {
53
+            switch ($this->driver) {
54
+                case 'mysql':
55
+                case 'pgsql':
56
+                    return "ST_AsText($value) as $value";
57
+                case 'sqlsrv':
58
+                    return "REPLACE($value.STAsText(),' (','(') as $value";
59
+            }
60
+        }
61
+        return $value;
62
+    }
63
+
64
+}

+ 106
- 0
src/Tqdev/PhpCrudApi/Database/ColumnsBuilder.php View File

@@ -0,0 +1,106 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Database;
3
+
4
+use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
5
+use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
6
+
7
+class ColumnsBuilder
8
+{
9
+    private $driver;
10
+    private $converter;
11
+
12
+    public function __construct(String $driver)
13
+    {
14
+        $this->driver = $driver;
15
+        $this->converter = new ColumnConverter($driver);
16
+    }
17
+
18
+    public function getOffsetLimit(int $offset, int $limit): String
19
+    {
20
+        if ($limit < 0 || $offset < 0) {
21
+            return '';
22
+        }
23
+        switch ($this->driver) {
24
+            case 'mysql':return "LIMIT $offset, $limit";
25
+            case 'pgsql':return "LIMIT $limit OFFSET $offset";
26
+            case 'sqlsrv':return "OFFSET $offset ROWS FETCH NEXT $limit ROWS ONLY";
27
+        }
28
+    }
29
+
30
+    private function quoteColumnName(ReflectedColumn $column): String
31
+    {
32
+        return '"' . $column->getName() . '"';
33
+    }
34
+
35
+    public function getOrderBy(ReflectedTable $table, array $columnOrdering): String
36
+    {
37
+        $results = array();
38
+        foreach ($columnOrdering as $i => list($columnName, $ordering)) {
39
+            $column = $table->get($columnName);
40
+            $quotedColumnName = $this->quoteColumnName($column);
41
+            $results[] = $quotedColumnName . ' ' . $ordering;
42
+        }
43
+        return implode(',', $results);
44
+    }
45
+
46
+    public function getSelect(ReflectedTable $table, array $columnNames): String
47
+    {
48
+        $results = array();
49
+        foreach ($columnNames as $columnName) {
50
+            $column = $table->get($columnName);
51
+            $quotedColumnName = $this->quoteColumnName($column);
52
+            $quotedColumnName = $this->converter->convertColumnName($column, $quotedColumnName);
53
+            $results[] = $quotedColumnName;
54
+        }
55
+        return implode(',', $results);
56
+    }
57
+
58
+    public function getInsert(ReflectedTable $table, array $columnValues): String
59
+    {
60
+        $columns = array();
61
+        $values = array();
62
+        foreach ($columnValues as $columnName => $columnValue) {
63
+            $column = $table->get($columnName);
64
+            $quotedColumnName = $this->quoteColumnName($column);
65
+            $columns[] = $quotedColumnName;
66
+            $columnValue = $this->converter->convertColumnValue($column);
67
+            $values[] = $columnValue;
68
+        }
69
+        $columnsSql = '(' . implode(',', $columns) . ')';
70
+        $valuesSql = '(' . implode(',', $values) . ')';
71
+        $outputColumn = $this->quoteColumnName($table->getPk());
72
+        switch ($this->driver) {
73
+            case 'mysql':return "$columnsSql VALUES $valuesSql";
74
+            case 'pgsql':return "$columnsSql VALUES $valuesSql RETURNING $outputColumn";
75
+            case 'sqlsrv':return "$columnsSql OUTPUT INSERTED.$outputColumn VALUES $valuesSql";
76
+        }
77
+    }
78
+
79
+    public function getUpdate(ReflectedTable $table, array $columnValues): String
80
+    {
81
+        $results = array();
82
+        foreach ($columnValues as $columnName => $columnValue) {
83
+            $column = $table->get($columnName);
84
+            $quotedColumnName = $this->quoteColumnName($column);
85
+            $columnValue = $this->converter->convertColumnValue($column);
86
+            $results[] = $quotedColumnName . '=' . $columnValue;
87
+        }
88
+        return implode(',', $results);
89
+    }
90
+
91
+    public function getIncrement(ReflectedTable $table, array $columnValues): String
92
+    {
93
+        $results = array();
94
+        foreach ($columnValues as $columnName => $columnValue) {
95
+            if (!is_numeric($columnValue)) {
96
+                continue;
97
+            }
98
+            $column = $table->get($columnName);
99
+            $quotedColumnName = $this->quoteColumnName($column);
100
+            $columnValue = $this->converter->convertColumnValue($column);
101
+            $results[] = $quotedColumnName . '=' . $quotedColumnName . '+' . $columnValue;
102
+        }
103
+        return implode(',', $results);
104
+    }
105
+
106
+}

+ 202
- 0
src/Tqdev/PhpCrudApi/Database/ConditionsBuilder.php View File

@@ -0,0 +1,202 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Database;
3
+
4
+use Tqdev\PhpCrudApi\Record\Condition\AndCondition;
5
+use Tqdev\PhpCrudApi\Record\Condition\ColumnCondition;
6
+use Tqdev\PhpCrudApi\Record\Condition\Condition;
7
+use Tqdev\PhpCrudApi\Record\Condition\NoCondition;
8
+use Tqdev\PhpCrudApi\Record\Condition\NotCondition;
9
+use Tqdev\PhpCrudApi\Record\Condition\OrCondition;
10
+use Tqdev\PhpCrudApi\Record\Condition\SpatialCondition;
11
+use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
12
+
13
+class ConditionsBuilder
14
+{
15
+    private $driver;
16
+
17
+    public function __construct(String $driver)
18
+    {
19
+        $this->driver = $driver;
20
+    }
21
+
22
+    private function getConditionSql(Condition $condition, array &$arguments): String
23
+    {
24
+        if ($condition instanceof AndCondition) {
25
+            return $this->getAndConditionSql($condition, $arguments);
26
+        }
27
+        if ($condition instanceof OrCondition) {
28
+            return $this->getOrConditionSql($condition, $arguments);
29
+        }
30
+        if ($condition instanceof NotCondition) {
31
+            return $this->getNotConditionSql($condition, $arguments);
32
+        }
33
+        if ($condition instanceof ColumnCondition) {
34
+            return $this->getColumnConditionSql($condition, $arguments);
35
+        }
36
+        if ($condition instanceof SpatialCondition) {
37
+            return $this->getSpatialConditionSql($condition, $arguments);
38
+        }
39
+        throw new \Exception('Unknown Condition: ' . get_class($condition));
40
+    }
41
+
42
+    private function getAndConditionSql(AndCondition $and, array &$arguments): String
43
+    {
44
+        $parts = [];
45
+        foreach ($and->getConditions() as $condition) {
46
+            $parts[] = $this->getConditionSql($condition, $arguments);
47
+        }
48
+        return '(' . implode(' AND ', $parts) . ')';
49
+    }
50
+
51
+    private function getOrConditionSql(OrCondition $or, array &$arguments): String
52
+    {
53
+        $parts = [];
54
+        foreach ($or->getConditions() as $condition) {
55
+            $parts[] = $this->getConditionSql($condition, $arguments);
56
+        }
57
+        return '(' . implode(' OR ', $parts) . ')';
58
+    }
59
+
60
+    private function getNotConditionSql(NotCondition $not, array &$arguments): String
61
+    {
62
+        $condition = $not->getCondition();
63
+        return '(NOT ' . $this->getConditionSql($condition, $arguments) . ')';
64
+    }
65
+
66
+    private function quoteColumnName(ReflectedColumn $column): String
67
+    {
68
+        return '"' . $column->getName() . '"';
69
+    }
70
+
71
+    private function escapeLikeValue(String $value): String
72
+    {
73
+        return addcslashes($value, '%_');
74
+    }
75
+
76
+    private function getColumnConditionSql(ColumnCondition $condition, array &$arguments): String
77
+    {
78
+        $column = $this->quoteColumnName($condition->getColumn());
79
+        $operator = $condition->getOperator();
80
+        $value = $condition->getValue();
81
+        switch ($operator) {
82
+            case 'cs':
83
+                $sql = "$column LIKE ?";
84
+                $arguments[] = '%' . $this->escapeLikeValue($value) . '%';
85
+                break;
86
+            case 'sw':
87
+                $sql = "$column LIKE ?";
88
+                $arguments[] = $this->escapeLikeValue($value) . '%';
89
+                break;
90
+            case 'ew':
91
+                $sql = "$column LIKE ?";
92
+                $arguments[] = '%' . $this->escapeLikeValue($value);
93
+                break;
94
+            case 'eq':
95
+                $sql = "$column = ?";
96
+                $arguments[] = $value;
97
+                break;
98
+            case 'lt':
99
+                $sql = "$column < ?";
100
+                $arguments[] = $value;
101
+                break;
102
+            case 'le':
103
+                $sql = "$column <= ?";
104
+                $arguments[] = $value;
105
+                break;
106
+            case 'ge':
107
+                $sql = "$column >= ?";
108
+                $arguments[] = $value;
109
+                break;
110
+            case 'gt':
111
+                $sql = "$column > ?";
112
+                $arguments[] = $value;
113
+                break;
114
+            case 'bt':
115
+                $parts = explode(',', $value, 2);
116
+                $count = count($parts);
117
+                if ($count == 2) {
118
+                    $sql = "($column >= ? AND $column <= ?)";
119
+                    $arguments[] = $parts[0];
120
+                    $arguments[] = $parts[1];
121
+                } else {
122
+                    $sql = "FALSE";
123
+                }
124
+                break;
125
+            case 'in':
126
+                $parts = explode(',', $value);
127
+                $count = count($parts);
128
+                if ($count > 0) {
129
+                    $qmarks = implode(',', str_split(str_repeat('?', $count)));
130
+                    $sql = "$column IN ($qmarks)";
131
+                    for ($i = 0; $i < $count; $i++) {
132
+                        $arguments[] = $parts[$i];
133
+                    }
134
+                } else {
135
+                    $sql = "FALSE";
136
+                }
137
+                break;
138
+            case 'is':
139
+                $sql = "$column IS NULL";
140
+                break;
141
+        }
142
+        return $sql;
143
+    }
144
+
145
+    private function getSpatialFunctionName(String $operator): String
146
+    {
147
+        switch ($operator) {
148
+            case 'co':return 'ST_Contains';
149
+            case 'cr':return 'ST_Crosses';
150
+            case 'di':return 'ST_Disjoint';
151
+            case 'eq':return 'ST_Equals';
152
+            case 'in':return 'ST_Intersects';
153
+            case 'ov':return 'ST_Overlaps';
154
+            case 'to':return 'ST_Touches';
155
+            case 'wi':return 'ST_Within';
156
+            case 'ic':return 'ST_IsClosed';
157
+            case 'is':return 'ST_IsSimple';
158
+            case 'iv':return 'ST_IsValid';
159
+        }
160
+    }
161
+
162
+    private function hasSpatialArgument(String $operator): bool
163
+    {
164
+        return in_array($opertor, ['ic', 'is', 'iv']) ? false : true;
165
+    }
166
+
167
+    private function getSpatialFunctionCall(String $functionName, String $column, bool $hasArgument): String
168
+    {
169
+        switch ($this->driver) {
170
+            case 'mysql':
171
+            case 'pgsql':
172
+                $argument = $hasArgument ? 'ST_GeomFromText(?)' : '';
173
+                return "$functionName($column, $argument)=TRUE";
174
+            case 'sql_srv':
175
+                $functionName = str_replace('ST_', 'ST', $functionName);
176
+                $argument = $hasArgument ? 'geometry::STGeomFromText(?,0)' : '';
177
+                return "$column.$functionName($argument)=1";
178
+        }
179
+    }
180
+
181
+    private function getSpatialConditionSql(ColumnCondition $condition, array &$arguments): String
182
+    {
183
+        $column = $this->quoteColumnName($condition->getColumn());
184
+        $operator = $condition->getOperator();
185
+        $value = $condition->getValue();
186
+        $functionName = $this->getSpatialFunctionName($operator);
187
+        $hasArgument = $this->hasSpatialArgument($operator);
188
+        $sql = $this->getSpatialFunctionCall($functionName, $column, $hasArgument);
189
+        if ($hasArgument) {
190
+            $arguments[] = $value;
191
+        }
192
+        return $sql;
193
+    }
194
+
195
+    public function getWhereClause(Condition $condition, array &$arguments): String
196
+    {
197
+        if ($condition instanceof NoCondition) {
198
+            return '';
199
+        }
200
+        return ' WHERE ' . $this->getConditionSql($condition, $arguments);
201
+    }
202
+}

+ 81
- 0
src/Tqdev/PhpCrudApi/Database/DataConverter.php View File

@@ -0,0 +1,81 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Database;
3
+
4
+use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
5
+use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
6
+
7
+class DataConverter
8
+{
9
+    private $driver;
10
+
11
+    public function __construct(String $driver)
12
+    {
13
+        $this->driver = $driver;
14
+    }
15
+
16
+    private function convertRecordValue($conversion, $value)
17
+    {
18
+        switch ($conversion) {
19
+            case 'boolean':
20
+                return $value ? true : false;
21
+        }
22
+        return $value;
23
+    }
24
+
25
+    private function getRecordValueConversion(ReflectedColumn $column): String
26
+    {
27
+        if (in_array($this->driver, ['mysql', 'sqlsrv']) && $column->isBoolean()) {
28
+            return 'boolean';
29
+        }
30
+        return 'none';
31
+    }
32
+
33
+    public function convertRecords(ReflectedTable $table, array $columnNames, array &$records) /*: void*/
34
+    {
35
+        foreach ($columnNames as $columnName) {
36
+            $column = $table->get($columnName);
37
+            $conversion = $this->getRecordValueConversion($column);
38
+            if ($conversion != 'none') {
39
+                foreach ($records as $i => $record) {
40
+                    $value = $records[$i][$columnName];
41
+                    if ($value === null) {
42
+                        continue;
43
+                    }
44
+                    $records[$i][$columnName] = $this->convertRecordValue($conversion, $value);
45
+                }
46
+            }
47
+        }
48
+    }
49
+
50
+    private function convertInputValue($conversion, $value)
51
+    {
52
+        switch ($conversion) {
53
+            case 'base64url_to_base64':
54
+                return str_pad(strtr($value, '-_', '+/'), ceil(strlen($value) / 4) * 4, '=', STR_PAD_RIGHT);
55
+        }
56
+        return $value;
57
+    }
58
+
59
+    private function getInputValueConversion(ReflectedColumn $column): String
60
+    {
61
+        if ($column->isBinary()) {
62
+            return 'base64url_to_base64';
63
+        }
64
+        return 'none';
65
+    }
66
+
67
+    public function convertColumnValues(ReflectedTable $table, array &$columnValues) /*: void*/
68
+    {
69
+        $columnNames = array_keys($columnValues);
70
+        foreach ($columnNames as $columnName) {
71
+            $column = $table->get($columnName);
72
+            $conversion = $this->getInputValueConversion($column);
73
+            if ($conversion != 'none') {
74
+                $value = $columnValues[$columnName];
75
+                if ($value !== null) {
76
+                    $columnValues[$columnName] = $this->convertInputValue($conversion, $value);
77
+                }
78
+            }
79
+        }
80
+    }
81
+}

+ 228
- 0
src/Tqdev/PhpCrudApi/Database/GenericDB.php View File

@@ -0,0 +1,228 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Database;
3
+
4
+use Tqdev\PhpCrudApi\Record\Condition\ColumnCondition;
5
+use Tqdev\PhpCrudApi\Record\Condition\Condition;
6
+use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
7
+
8
+class GenericDB
9
+{
10
+    private $driver;
11
+    private $database;
12
+    private $pdo;
13
+    private $reflection;
14
+    private $columns;
15
+    private $conditions;
16
+    private $converter;
17
+
18
+    private function getDsn(String $address, String $port = null, String $database = null): String
19
+    {
20
+        switch ($this->driver) {
21
+            case 'mysql':return "$this->driver:host=$address;port=$port;dbname=$database;charset=utf8mb4";
22
+            case 'pgsql':return "$this->driver:host=$address port=$port dbname=$database options='--client_encoding=UTF8'";
23
+            case 'sqlsrv':return "$this->driver:Server=$address,$port;Database=$database";
24
+        }
25
+    }
26
+
27
+    private function getCommands(): array
28
+    {
29
+        switch ($this->driver) {
30
+            case 'mysql':return [
31
+                    'SET SESSION sql_warnings=1;',
32
+                    'SET NAMES utf8mb4;',
33
+                    'SET SESSION sql_mode = "ANSI,TRADITIONAL";',
34
+                ];
35
+            case 'pgsql':return [
36
+                    "SET NAMES 'UTF8';",
37
+                ];
38
+            case 'sqlsrv':return [
39
+                ];
40
+        }
41
+    }
42
+
43
+    private function getOptions(): array
44
+    {
45
+        $options = array(
46
+            \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
47
+            \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
48
+        );
49
+        switch ($this->driver) {
50
+            case 'mysql':return $options + [
51
+                    \PDO::ATTR_EMULATE_PREPARES => false,
52
+                    \PDO::MYSQL_ATTR_FOUND_ROWS => true,
53
+                ];
54
+            case 'pgsql':return $options + [
55
+                    \PDO::ATTR_EMULATE_PREPARES => false,
56
+                ];
57
+            case 'sqlsrv':return $options + [
58
+                    \PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE => true,
59
+                ];
60
+        }
61
+    }
62
+
63
+    public function __construct(String $driver, String $address, String $port = null, String $database = null, String $username = null, String $password = null)
64
+    {
65
+        $this->driver = $driver;
66
+        $this->database = $database;
67
+        $dsn = $this->getDsn($address, $port, $database);
68
+        $options = $this->getOptions();
69
+        $this->pdo = new \PDO($dsn, $username, $password, $options);
70
+        $commands = $this->getCommands();
71
+        foreach ($commands as $command) {
72
+            $this->pdo->query($command);
73
+        }
74
+        $this->reflection = new GenericReflection($this->pdo, $driver, $database);
75
+        $this->definition = new GenericDefinition($this->pdo, $driver, $database);
76
+        $this->conditions = new ConditionsBuilder($driver);
77
+        $this->columns = new ColumnsBuilder($driver);
78
+        $this->converter = new DataConverter($driver);
79
+    }
80
+
81
+    public function pdo(): \PDO
82
+    {
83
+        return $this->pdo;
84
+    }
85
+
86
+    public function reflection(): GenericReflection
87
+    {
88
+        return $this->reflection;
89
+    }
90
+
91
+    public function definition(): GenericDefinition
92
+    {
93
+        return $this->definition;
94
+    }
95
+
96
+    public function createSingle(ReflectedTable $table, array $columnValues) /*: ?String*/
97
+    {
98
+        $this->converter->convertColumnValues($table, $columnValues);
99
+        $insertColumns = $this->columns->getInsert($table, $columnValues);
100
+        $tableName = $table->getName();
101
+        $pkName = $table->getPk()->getName();
102
+        $parameters = array_values($columnValues);
103
+        $sql = 'INSERT INTO "' . $tableName . '" ' . $insertColumns;
104
+        $stmt = $this->query($sql, $parameters);
105
+        // return primary key value if specified in the input
106
+        if (isset($columnValues[$pkName])) {
107
+            return $columnValues[$pkName];
108
+        }
109
+        // work around missing "returning" or "output" in mysql
110
+        switch ($this->driver) {
111
+            case 'mysql':
112
+                $stmt = $this->query('SELECT LAST_INSERT_ID()', []);
113
+                break;
114
+        }
115
+        return $stmt->fetchColumn(0);
116
+    }
117
+
118
+    public function selectSingle(ReflectedTable $table, array $columnNames, String $id) /*: ?array*/
119
+    {
120
+        $selectColumns = $this->columns->getSelect($table, $columnNames);
121
+        $tableName = $table->getName();
122
+        $condition = new ColumnCondition($table->getPk(), 'eq', $id);
123
+        $parameters = array();
124
+        $whereClause = $this->conditions->getWhereClause($condition, $parameters);
125
+        $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '" ' . $whereClause;
126
+        $stmt = $this->query($sql, $parameters);
127
+        $record = $stmt->fetch() ?: null;
128
+        if ($record === null) {
129
+            return null;
130
+        }
131
+        $records = array($record);
132
+        $this->converter->convertRecords($table, $columnNames, $records);
133
+        return $records[0];
134
+    }
135
+
136
+    public function selectMultiple(ReflectedTable $table, array $columnNames, array $ids): array
137
+    {
138
+        if (count($ids) == 0) {
139
+            return [];
140
+        }
141
+        $selectColumns = $this->columns->getSelect($table, $columnNames);
142
+        $tableName = $table->getName();
143
+        $condition = new ColumnCondition($table->getPk(), 'in', implode(',', $ids));
144
+        $parameters = array();
145
+        $whereClause = $this->conditions->getWhereClause($condition, $parameters);
146
+        $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '" ' . $whereClause;
147
+        $stmt = $this->query($sql, $parameters);
148
+        $records = $stmt->fetchAll();
149
+        $this->converter->convertRecords($table, $columnNames, $records);
150
+        return $records;
151
+    }
152
+
153
+    public function selectCount(ReflectedTable $table, Condition $condition): int
154
+    {
155
+        $tableName = $table->getName();
156
+        $parameters = array();
157
+        $whereClause = $this->conditions->getWhereClause($condition, $parameters);
158
+        $sql = 'SELECT COUNT(*) FROM "' . $tableName . '"' . $whereClause;
159
+        $stmt = $this->query($sql, $parameters);
160
+        return $stmt->fetchColumn(0);
161
+    }
162
+
163
+    public function selectAllUnordered(ReflectedTable $table, array $columnNames, Condition $condition): array
164
+    {
165
+        $selectColumns = $this->columns->getSelect($table, $columnNames);
166
+        $tableName = $table->getName();
167
+        $parameters = array();
168
+        $whereClause = $this->conditions->getWhereClause($condition, $parameters);
169
+        $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '"' . $whereClause;
170
+        $stmt = $this->query($sql, $parameters);
171
+        $records = $stmt->fetchAll();
172
+        $this->converter->convertRecords($table, $columnNames, $records);
173
+        return $records;
174
+    }
175
+
176
+    public function selectAll(ReflectedTable $table, array $columnNames, Condition $condition, array $columnOrdering, int $offset, int $limit): array
177
+    {
178
+        if ($limit == 0) {
179
+            return array();
180
+        }
181
+        $selectColumns = $this->columns->getSelect($table, $columnNames);
182
+        $tableName = $table->getName();
183
+        $parameters = array();
184
+        $whereClause = $this->conditions->getWhereClause($condition, $parameters);
185
+        $orderBy = $this->columns->getOrderBy($table, $columnOrdering);
186
+        $offsetLimit = $this->columns->getOffsetLimit($offset, $limit);
187
+        $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '"' . $whereClause . ' ORDER BY ' . $orderBy . ' ' . $offsetLimit;
188
+        $stmt = $this->query($sql, $parameters);
189
+        $records = $stmt->fetchAll();
190
+        $this->converter->convertRecords($table, $columnNames, $records);
191
+        return $records;
192
+    }
193
+
194
+    public function updateSingle(ReflectedTable $table, array $columnValues, String $id)
195
+    {
196
+        if (count($columnValues) == 0) {
197
+            return 0;
198
+        }
199
+        $this->converter->convertColumnValues($table, $columnValues);
200
+        $updateColumns = $this->columns->getUpdate($table, $columnValues);
201
+        $tableName = $table->getName();
202
+        $condition = new ColumnCondition($table->getPk(), 'eq', $id);
203
+        $parameters = array_values($columnValues);
204
+        $whereClause = $this->conditions->getWhereClause($condition, $parameters);
205
+        $sql = 'UPDATE "' . $tableName . '" SET ' . $updateColumns . $whereClause;
206
+        $stmt = $this->query($sql, $parameters);
207
+        return $stmt->rowCount();
208
+    }
209
+
210
+    public function deleteSingle(ReflectedTable $table, String $id)
211
+    {
212
+        $tableName = $table->getName();
213
+        $condition = new ColumnCondition($table->getPk(), 'eq', $id);
214
+        $parameters = array();
215
+        $whereClause = $this->conditions->getWhereClause($condition, $parameters);
216
+        $sql = 'DELETE FROM "' . $tableName . '" ' . $whereClause;
217
+        $stmt = $this->query($sql, $parameters);
218
+        return $stmt->rowCount();
219
+    }
220
+
221
+    private function query(String $sql, array $parameters): \PDOStatement
222
+    {
223
+        $stmt = $this->pdo->prepare($sql);
224
+        $stmt->execute($parameters);
225
+        //echo "- $sql -- " . json_encode($parameters, JSON_UNESCAPED_UNICODE) . "\n";
226
+        return $stmt;
227
+    }
228
+}

+ 411
- 0
src/Tqdev/PhpCrudApi/Database/GenericDefinition.php View File

@@ -0,0 +1,411 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Database;
3
+
4
+use Tqdev\PhpCrudApi\Column\Reflection\ReflectedColumn;
5
+use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
6
+
7
+class GenericDefinition
8
+{
9
+    private $pdo;
10
+    private $driver;
11
+    private $database;
12
+    private $typeConverter;
13
+    private $reflection;
14
+
15
+    public function __construct(\PDO $pdo, String $driver, String $database)
16
+    {
17
+        $this->pdo = $pdo;
18
+        $this->driver = $driver;
19
+        $this->database = $database;
20
+        $this->typeConverter = new TypeConverter($driver);
21
+        $this->reflection = new GenericReflection($pdo, $driver, $database);
22
+    }
23
+
24
+    private function quote(String $identifier): String
25
+    {
26
+        return '"' . str_replace('"', '', $identifier) . '"';
27
+    }
28
+
29
+    public function getColumnType(ReflectedColumn $column, bool $update): String
30
+    {
31
+        if ($this->driver == 'pgsql' && !$update && $column->getPk() && $this->canAutoIncrement($column)) {
32
+            return 'serial';
33
+        }
34
+        $type = $this->typeConverter->fromJdbc($column->getType(), $column->getPk());
35
+        if ($column->hasPrecision() && $column->hasScale()) {
36
+            $size = '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
37
+        } else if ($column->hasPrecision()) {
38
+            $size = '(' . $column->getPrecision() . ')';
39
+        } else if ($column->hasLength()) {
40
+            $size = '(' . $column->getLength() . ')';
41
+        } else {
42
+            $size = '';
43
+        }
44
+        $null = $this->getColumnNullType($column, $update);
45
+        $auto = $this->getColumnAutoIncrement($column, $update);
46
+        return $type . $size . $null . $auto;
47
+    }
48
+
49
+    private function getPrimaryKey(String $tableName): String
50
+    {
51
+        $pks = $this->reflection->getTablePrimaryKeys($tableName);
52
+        if (count($pks) == 1) {
53
+            return $pks[0];
54
+        }
55
+        return "";
56
+    }
57
+
58
+    private function canAutoIncrement(ReflectedColumn $column): bool
59
+    {
60
+        return in_array($column->getType(), ['integer', 'bigint']);
61
+    }
62
+
63
+    private function getColumnAutoIncrement(ReflectedColumn $column, bool $update): String
64
+    {
65
+        if (!$this->canAutoIncrement($column)) {
66
+            return '';
67
+        }
68
+        switch ($this->driver) {
69
+            case 'mysql':
70
+                return $column->getPk() ? ' AUTO_INCREMENT' : '';
71
+            case 'pgsql':
72
+                return '';
73
+            case 'sqlsrv':
74
+                return ($column->getPk() && !$update) ? ' IDENTITY(1,1)' : '';
75
+        }
76
+    }
77
+
78
+    private function getColumnNullType(ReflectedColumn $column, bool $update): String
79
+    {
80
+        if ($this->driver == 'pgsql' && $update) {
81
+            return '';
82
+        }
83
+        return $column->getNullable() ? ' NULL' : ' NOT NULL';
84
+    }
85
+
86
+    private function getTableRenameSQL(String $tableName, String $newTableName): String
87
+    {
88
+        $p1 = $this->quote($tableName);
89
+        $p2 = $this->quote($newTableName);
90
+
91
+        switch ($this->driver) {
92
+            case 'mysql':
93
+                return "RENAME TABLE $p1 TO $p2";
94
+            case 'pgsql':
95
+                return "ALTER TABLE $p1 RENAME TO $p2";
96
+            case 'sqlsrv':
97
+                return "EXEC sp_rename $p1, $p2";
98
+        }
99
+    }
100
+
101
+    private function getColumnRenameSQL(String $tableName, String $columnName, ReflectedColumn $newColumn): String
102
+    {
103
+        $p1 = $this->quote($tableName);
104
+        $p2 = $this->quote($columnName);
105
+        $p3 = $this->quote($newColumn->getName());
106
+
107
+        switch ($this->driver) {
108
+            case 'mysql':
109
+                $p4 = $this->getColumnType($newColumn, true);
110
+                return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
111
+            case 'pgsql':
112
+                return "ALTER TABLE $p1 RENAME COLUMN $p2 TO $p3";
113
+            case 'sqlsrv':
114
+                $p4 = $this->quote($tableName . '.' . $columnName);
115
+                return "EXEC sp_rename $p4, $p3, 'COLUMN'";
116
+        }
117
+    }
118
+
119
+    private function getColumnRetypeSQL(String $tableName, String $columnName, ReflectedColumn $newColumn): String
120
+    {
121
+        $p1 = $this->quote($tableName);
122
+        $p2 = $this->quote($columnName);
123
+        $p3 = $this->quote($newColumn->getName());
124
+        $p4 = $this->getColumnType($newColumn, true);
125
+
126
+        switch ($this->driver) {
127
+            case 'mysql':
128
+                return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
129
+            case 'pgsql':
130
+                return "ALTER TABLE $p1 ALTER COLUMN $p3 TYPE $p4";
131
+            case 'sqlsrv':
132
+                return "ALTER TABLE $p1 ALTER COLUMN $p3 $p4";
133
+        }
134
+    }
135
+
136
+    private function getSetColumnNullableSQL(String $tableName, String $columnName, ReflectedColumn $newColumn): String
137
+    {
138
+        $p1 = $this->quote($tableName);
139
+        $p2 = $this->quote($columnName);
140
+        $p3 = $this->quote($newColumn->getName());
141
+        $p4 = $this->getColumnType($newColumn, true);
142
+
143
+        switch ($this->driver) {
144
+            case 'mysql':
145
+                return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
146
+            case 'pgsql':
147
+                $p5 = $newColumn->getNullable() ? 'DROP NOT NULL' : 'SET NOT NULL';
148
+                return "ALTER TABLE $p1 ALTER COLUMN $p2 $p5";
149
+            case 'sqlsrv':
150
+                return "ALTER TABLE $p1 ALTER COLUMN $p2 $p4";
151
+        }
152
+    }
153
+
154
+    private function getSetColumnPkConstraintSQL(String $tableName, String $columnName, ReflectedColumn $newColumn): String
155
+    {
156
+        $p1 = $this->quote($tableName);
157
+        $p2 = $this->quote($columnName);
158
+        $p3 = $this->quote($tableName . '_pkey');
159
+
160
+        switch ($this->driver) {
161
+            case 'mysql':
162
+                $p4 = $newColumn->getPk() ? "ADD PRIMARY KEY ($p2)" : 'DROP PRIMARY KEY';
163
+                return "ALTER TABLE $p1 $p4";
164
+            case 'pgsql':
165
+            case 'sqlsrv':
166
+                $p4 = $newColumn->getPk() ? "ADD PRIMARY KEY ($p2)" : "DROP CONSTRAINT $p3";
167
+                return "ALTER TABLE $p1 $p4";
168
+        }
169
+    }
170
+
171
+    private function getSetColumnPkSequenceSQL(String $tableName, String $columnName, ReflectedColumn $newColumn): String
172
+    {
173
+        $p1 = $this->quote($tableName);
174
+        $p2 = $this->quote($columnName);
175
+        $p3 = $this->quote($tableName . '_' . $columnName . '_seq');
176
+
177
+        switch ($this->driver) {
178
+            case 'mysql':
179
+                return "select 1";
180
+            case 'pgsql':
181
+                return $newColumn->getPk() ? "CREATE SEQUENCE $p3 OWNED BY $p1.$p2" : "DROP SEQUENCE $p3";
182
+            case 'sqlsrv':
183
+                return $newColumn->getPk() ? "CREATE SEQUENCE $p3" : "DROP SEQUENCE $p3";
184
+        }
185
+    }
186
+
187
+    private function getSetColumnPkSequenceStartSQL(String $tableName, String $columnName, ReflectedColumn $newColumn): String
188
+    {
189
+        $p1 = $this->quote($tableName);
190
+        $p2 = $this->quote($columnName);
191
+        $p3 = $this->pdo->quote($tableName . '_' . $columnName . '_seq');
192
+
193
+        switch ($this->driver) {
194
+            case 'mysql':
195
+                return "select 1";
196
+            case 'pgsql':
197
+                return "SELECT setval($p3, (SELECT max($p2)+1 FROM $p1));";
198
+            case 'sqlsrv':
199
+                return "ALTER SEQUENCE $p3 RESTART WITH (SELECT max($p2)+1 FROM $p1)";
200
+        }
201
+    }
202
+
203
+    private function getSetColumnPkDefaultSQL(String $tableName, String $columnName, ReflectedColumn $newColumn): String
204
+    {
205
+        $p1 = $this->quote($tableName);
206
+        $p2 = $this->quote($columnName);
207
+
208
+        switch ($this->driver) {
209
+            case 'mysql':
210
+                $p3 = $this->quote($newColumn->getName());
211
+                $p4 = $this->getColumnType($newColumn, true);
212
+                return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
213
+            case 'pgsql':
214
+                if ($newColumn->getPk()) {
215
+                    $p3 = $this->pdo->quote($tableName . '_' . $columnName . '_seq');
216
+                    $p4 = "SET DEFAULT nextval($p3)";
217
+                } else {
218
+                    $p4 = 'DROP DEFAULT';
219
+                }
220
+                return "ALTER TABLE $p1 ALTER COLUMN $p2 $p4";
221
+            case 'sqlsrv':
222
+                $p3 = $this->pdo->quote($tableName . '_' . $columnName . '_seq');
223
+                $p4 = $this->quote('DF_' . $tableName . '_' . $columnName);
224
+                if ($newColumn->getPk()) {
225
+                    return "ALTER TABLE $p1 ADD CONSTRAINT $p4 DEFAULT NEXT VALUE FOR $p3 FOR $p2";
226
+                } else {
227
+                    return "ALTER TABLE $p1 DROP CONSTRAINT $p4";
228
+                }
229
+        }
230
+    }
231
+
232
+    private function getAddColumnFkConstraintSQL(String $tableName, String $columnName, ReflectedColumn $newColumn): String
233
+    {
234
+        $p1 = $this->quote($tableName);
235
+        $p2 = $this->quote($columnName);
236
+        $p3 = $this->quote($tableName . '_' . $columnName . '_fkey');
237
+        $p4 = $this->quote($newColumn->getFk());
238
+        $p5 = $this->quote($this->getPrimaryKey($newColumn->getFk()));
239
+
240
+        return "ALTER TABLE $p1 ADD CONSTRAINT $p3 FOREIGN KEY ($p2) REFERENCES $p4 ($p5)";
241
+    }
242
+
243
+    private function getRemoveColumnFkConstraintSQL(String $tableName, String $columnName, ReflectedColumn $newColumn): String
244
+    {
245
+        $p1 = $this->quote($tableName);
246
+        $p2 = $this->quote($tableName . '_' . $columnName . '_fkey');
247
+
248
+        switch ($this->driver) {
249
+            case 'mysql':
250
+                return "ALTER TABLE $p1 DROP FOREIGN KEY $p2";
251
+            case 'pgsql':
252
+            case 'sqlsrv':
253
+                return "ALTER TABLE $p1 DROP CONSTRAINT $p2";
254
+        }
255
+    }
256
+
257
+    private function getAddTableSQL(ReflectedTable $newTable): String
258
+    {
259
+        $tableName = $newTable->getName();
260
+        $p1 = $this->quote($tableName);
261
+        $fields = [];
262
+        $constraints = [];
263
+        foreach ($newTable->columnNames() as $columnName) {
264
+            $newColumn = $newTable->get($columnName);
265
+            $f1 = $this->quote($columnName);
266
+            $f2 = $this->getColumnType($newColumn, false);
267
+            $f3 = $this->quote($tableName . '_' . $columnName . '_fkey');
268
+            $f4 = $this->quote($newColumn->getFk());
269
+            $f5 = $this->quote($this->getPrimaryKey($newColumn->getFk()));
270
+            $fields[] = "$f1 $f2";
271
+            if ($newColumn->getPk()) {
272
+                $constraints[] = "PRIMARY KEY ($f1)";
273
+            }
274
+            if ($newColumn->getFk()) {
275
+                $constraints[] = "CONSTRAINT $f3 FOREIGN KEY ($f1) REFERENCES $f4 ($f5)";
276
+            }
277
+        }
278
+        $p2 = implode(',', array_merge($fields, $constraints));
279
+
280
+        return "CREATE TABLE $p1 ($p2);";
281
+    }
282
+
283
+    private function getAddColumnSQL(String $tableName, ReflectedColumn $newColumn): String
284
+    {
285
+        $p1 = $this->quote($tableName);
286
+        $p2 = $this->quote($newColumn->getName());
287
+        $p3 = $this->getColumnType($newColumn, false);
288
+
289
+        return "ALTER TABLE $p1 ADD COLUMN $p2 $p3";
290
+    }
291
+
292
+    private function getRemoveTableSQL(String $tableName): String
293
+    {
294
+        $p1 = $this->quote($tableName);
295
+
296
+        return "DROP TABLE $p1 CASCADE;";
297
+    }
298
+
299
+    private function getRemoveColumnSQL(String $tableName, String $columnName): String
300
+    {
301
+        $p1 = $this->quote($tableName);
302
+        $p2 = $this->quote($columnName);
303
+
304
+        return "ALTER TABLE $p1 DROP COLUMN $p2 CASCADE;";
305
+    }
306
+
307
+    public function renameTable(String $tableName, String $newTableName)
308
+    {
309
+        $sql = $this->getTableRenameSQL($tableName, $newTableName);
310
+        $stmt = $this->pdo->prepare($sql);
311
+        return $stmt->execute();
312
+    }
313
+
314
+    public function renameColumn(String $tableName, String $columnName, ReflectedColumn $newColumn)
315
+    {
316
+        $sql = $this->getColumnRenameSQL($tableName, $columnName, $newColumn);
317
+        $stmt = $this->pdo->prepare($sql);
318
+        return $stmt->execute();
319
+    }
320
+
321
+    public function retypeColumn(String $tableName, String $columnName, ReflectedColumn $newColumn)
322
+    {
323
+        $sql = $this->getColumnRetypeSQL($tableName, $columnName, $newColumn);
324
+        $stmt = $this->pdo->prepare($sql);
325
+        return $stmt->execute();
326
+    }
327
+
328
+    public function setColumnNullable(String $tableName, String $columnName, ReflectedColumn $newColumn)
329
+    {
330
+        $sql = $this->getSetColumnNullableSQL($tableName, $columnName, $newColumn);
331
+        $stmt = $this->pdo->prepare($sql);
332
+        return $stmt->execute();
333
+    }
334
+
335
+    public function addColumnPrimaryKey(String $tableName, String $columnName, ReflectedColumn $newColumn)
336
+    {
337
+        $sql = $this->getSetColumnPkConstraintSQL($tableName, $columnName, $newColumn);
338
+        $stmt = $this->pdo->prepare($sql);
339
+        $stmt->execute();
340
+        if ($this->canAutoIncrement($newColumn)) {
341
+            $sql = $this->getSetColumnPkSequenceSQL($tableName, $columnName, $newColumn);
342
+            $stmt = $this->pdo->prepare($sql);
343
+            $stmt->execute();
344
+            $sql = $this->getSetColumnPkSequenceStartSQL($tableName, $columnName, $newColumn);
345
+            $stmt = $this->pdo->prepare($sql);
346
+            $stmt->execute();
347
+            $sql = $this->getSetColumnPkDefaultSQL($tableName, $columnName, $newColumn);
348
+            $stmt = $this->pdo->prepare($sql);
349
+            $stmt->execute();
350
+        }
351
+        return true;
352
+    }
353
+
354
+    public function removeColumnPrimaryKey(String $tableName, String $columnName, ReflectedColumn $newColumn)
355
+    {
356
+        if ($this->canAutoIncrement($newColumn)) {
357
+            $sql = $this->getSetColumnPkDefaultSQL($tableName, $columnName, $newColumn);
358
+            $stmt = $this->pdo->prepare($sql);
359
+            $stmt->execute();
360
+            $sql = $this->getSetColumnPkSequenceSQL($tableName, $columnName, $newColumn);
361
+            $stmt = $this->pdo->prepare($sql);
362
+            $stmt->execute();
363
+        }
364
+        $sql = $this->getSetColumnPkConstraintSQL($tableName, $columnName, $newColumn);
365
+        $stmt = $this->pdo->prepare($sql);
366
+        $stmt->execute();
367
+        return true;
368
+    }
369
+
370
+    public function addColumnForeignKey(String $tableName, String $columnName, ReflectedColumn $newColumn)
371
+    {
372
+        $sql = $this->getAddColumnFkConstraintSQL($tableName, $columnName, $newColumn);
373
+        $stmt = $this->pdo->prepare($sql);
374
+        return $stmt->execute();
375
+    }
376
+
377
+    public function removeColumnForeignKey(String $tableName, String $columnName, ReflectedColumn $newColumn)
378
+    {
379
+        $sql = $this->getRemoveColumnFkConstraintSQL($tableName, $columnName, $newColumn);
380
+        $stmt = $this->pdo->prepare($sql);
381
+        return $stmt->execute();
382
+    }
383
+
384
+    public function addTable(ReflectedTable $newTable)
385
+    {
386
+        $sql = $this->getAddTableSQL($newTable);
387
+        $stmt = $this->pdo->prepare($sql);
388
+        return $stmt->execute();
389
+    }
390
+
391
+    public function addColumn(String $tableName, ReflectedColumn $newColumn)
392
+    {
393
+        $sql = $this->getAddColumnSQL($tableName, $newColumn);
394
+        $stmt = $this->pdo->prepare($sql);
395
+        return $stmt->execute();
396
+    }
397
+
398
+    public function removeTable(String $tableName)
399
+    {
400
+        $sql = $this->getRemoveTableSQL($tableName);
401
+        $stmt = $this->pdo->prepare($sql);
402
+        return $stmt->execute();
403
+    }
404
+
405
+    public function removeColumn(String $tableName, String $columnName)
406
+    {
407
+        $sql = $this->getRemoveColumnSQL($tableName, $columnName);
408
+        $stmt = $this->pdo->prepare($sql);
409
+        return $stmt->execute();
410
+    }
411
+}

+ 111
- 0
src/Tqdev/PhpCrudApi/Database/GenericReflection.php View File

@@ -0,0 +1,111 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Database;
3
+
4
+class GenericReflection
5
+{
6
+    private $pdo;
7
+    private $driver;
8
+    private $database;
9
+    private $typeConverter;
10
+
11
+    public function __construct(\PDO $pdo, String $driver, String $database)
12
+    {
13
+        $this->pdo = $pdo;
14
+        $this->driver = $driver;
15
+        $this->database = $database;
16
+        $this->typeConverter = new TypeConverter($driver);
17
+    }
18
+
19
+    public function getIgnoredTables(): array
20
+    {
21
+        switch ($this->driver) {
22
+            case 'mysql':return [];
23
+            case 'pgsql':return ['spatial_ref_sys'];
24
+            case 'sqlsrv':return [];
25
+        }
26
+    }
27
+
28
+    private function getTablesSQL(): String
29
+    {
30
+        switch ($this->driver) {
31
+            case 'mysql':return 'SELECT "TABLE_NAME" FROM "INFORMATION_SCHEMA"."TABLES" WHERE "TABLE_TYPE" IN (\'BASE TABLE\') AND "TABLE_SCHEMA" = ? ORDER BY BINARY "TABLE_NAME"';
32
+            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";';
33
+            case 'sqlsrv':return 'SELECT o.name as "TABLE_NAME" FROM sysobjects o WHERE o.xtype = \'U\' ORDER BY "TABLE_NAME"';
34
+        }
35
+    }
36
+
37
+    private function getTableColumnsSQL(): String
38
+    {
39
+        switch ($this->driver) {
40
+            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" = ?';
41
+            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;';
42
+            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 \'\' <> ?';
43
+        }
44
+    }
45
+
46
+    private function getTablePrimaryKeysSQL(): String
47
+    {
48
+        switch ($this->driver) {
49
+            case 'mysql':return 'SELECT "COLUMN_NAME" FROM "INFORMATION_SCHEMA"."KEY_COLUMN_USAGE" WHERE "CONSTRAINT_NAME" = \'PRIMARY\' AND "TABLE_NAME" = ? AND "TABLE_SCHEMA" = ?';
50
+            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\'';
51
+            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 \'\' <> ?';
52
+        }
53
+    }
54
+
55
+    private function getTableForeignKeysSQL(): String
56
+    {
57
+        switch ($this->driver) {
58
+            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" = ?';
59
+            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\'';
60
+            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 \'\' <> ?';
61
+        }
62
+    }
63
+
64
+    public function getDatabaseName(): String
65
+    {
66
+        return $this->database;
67
+    }
68
+
69
+    public function getTables(): array
70
+    {
71
+        $stmt = $this->pdo->prepare($this->getTablesSQL());
72
+        $stmt->execute([$this->database]);
73
+        return $stmt->fetchAll();
74
+    }
75
+
76
+    public function getTableColumns(String $tableName): array
77
+    {
78
+        $stmt = $this->pdo->prepare($this->getTableColumnsSQL());
79
+        $stmt->execute([$tableName, $this->database]);
80
+        return $stmt->fetchAll();
81
+    }
82
+
83
+    public function getTablePrimaryKeys(String $tableName): array
84
+    {
85
+        $stmt = $this->pdo->prepare($this->getTablePrimaryKeysSQL());
86
+        $stmt->execute([$tableName, $this->database]);
87
+        $results = $stmt->fetchAll();
88
+        $primaryKeys = [];
89
+        foreach ($results as $result) {
90
+            $primaryKeys[] = $result['COLUMN_NAME'];
91
+        }
92
+        return $primaryKeys;
93
+    }
94
+
95
+    public function getTableForeignKeys(String $tableName): array
96
+    {
97
+        $stmt = $this->pdo->prepare($this->getTableForeignKeysSQL());
98
+        $stmt->execute([$tableName, $this->database]);
99
+        $results = $stmt->fetchAll();
100
+        $foreignKeys = [];
101
+        foreach ($results as $result) {
102
+            $foreignKeys[$result['COLUMN_NAME']] = $result['REFERENCED_TABLE_NAME'];
103
+        }
104
+        return $foreignKeys;
105
+    }
106
+
107
+    public function toJdbcType(String $type, int $size): String
108
+    {
109
+        return $this->typeConverter->toJdbc($type, $size);
110
+    }
111
+}

+ 187
- 0
src/Tqdev/PhpCrudApi/Database/TypeConverter.php View File

@@ -0,0 +1,187 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Database;
3
+
4
+class TypeConverter
5
+{
6
+    private $driver;
7
+
8
+    public function __construct(String $driver)
9
+    {
10
+        $this->driver = $driver;
11
+    }
12
+
13
+    private $fromJdbc = [
14
+        'mysql' => [
15
+            'clob' => 'longtext',
16
+            'boolean' => 'bit',
17
+            'blob' => 'longblob',
18
+            'timestamp' => 'datetime',
19
+        ],
20
+        'pgsql' => [
21
+            'clob' => 'text',
22
+            'blob' => 'bytea',
23
+        ],
24
+        'sqlsrv' => [
25
+            'boolean' => 'bit',
26
+        ],
27
+    ];
28
+
29
+    private $toJdbc = [
30
+        'simplified' => [
31
+            'char' => 'varchar',
32
+            'longvarchar' => 'clob',
33
+            'nchar' => 'varchar',
34
+            'nvarchar' => 'varchar',
35
+            'longnvarchar' => 'clob',
36
+            'binary' => 'varbinary',
37
+            'longvarbinary' => 'blob',
38
+            'tinyint' => 'integer',
39
+            'smallint' => 'integer',
40
+            'real' => 'float',
41
+            'numeric' => 'decimal',
42
+            'time_with_timezone' => 'time',
43
+            'timestamp_with_timezone' => 'timestamp',
44
+        ],
45
+        'mysql' => [
46
+            'tinyint(1)' => 'boolean',
47
+            'bit(0)' => 'boolean',
48
+            'bit(1)' => 'boolean',
49
+            'tinyblob' => 'blob',
50
+            'mediumblob' => 'blob',
51
+            'longblob' => 'blob',
52
+            'tinytext' => 'clob',
53
+            'mediumtext' => 'clob',
54
+            'longtext' => 'clob',
55
+            'text' => 'clob',
56
+            'int' => 'integer',
57
+            'polygon' => 'geometry',
58
+            'point' => 'geometry',
59
+            'datetime' => 'timestamp',
60
+        ],
61
+        'pgsql' => [
62
+            'bigserial' => 'bigint',
63
+            'bit varying' => 'bit',
64
+            'box' => 'geometry',
65
+            'bytea' => 'blob',
66
+            'character varying' => 'varchar',
67
+            'character' => 'char',
68
+            'cidr' => 'varchar',
69
+            'circle' => 'geometry',
70
+            'double precision' => 'double',
71
+            'inet' => 'integer',
72
+            //'interval [ fields ]'
73
+            'jsonb' => 'clob',
74
+            'line' => 'geometry',
75
+            'lseg' => 'geometry',
76
+            'macaddr' => 'varchar',
77
+            'money' => 'decimal',
78
+            'path' => 'geometry',
79
+            'point' => 'geometry',
80
+            'polygon' => 'geometry',
81
+            'real' => 'float',
82
+            'serial' => 'integer',
83
+            'text' => 'clob',
84
+            'time without time zone' => 'time',
85
+            'time with time zone' => 'time_with_timezone',
86
+            'timestamp without time zone' => 'timestamp',
87
+            'timestamp with time zone' => 'timestamp_with_timezone',
88
+            //'tsquery'=
89
+            //'tsvector'
90
+            //'txid_snapshot'
91
+            'uuid' => 'char',
92
+            'xml' => 'clob',
93
+        ],
94
+        // source: https://docs.microsoft.com/en-us/sql/connect/jdbc/using-basic-data-types?view=sql-server-2017
95
+        'sqlsrv' => [
96
+            'varbinary(0)' => 'blob',
97
+            'bit' => 'boolean',
98
+            'datetime' => 'timestamp',
99
+            'datetime2' => 'timestamp',
100
+            'float' => 'double',
101
+            'image' => 'longvarbinary',
102
+            'int' => 'integer',
103
+            'money' => 'decimal',
104
+            'ntext' => 'longnvarchar',
105
+            'smalldatetime' => 'timestamp',
106
+            'smallmoney' => 'decimal',
107
+            'text' => 'longvarchar',
108
+            'timestamp' => 'binary',
109
+            'tinyint' => 'tinyint',
110
+            'udt' => 'varbinary',
111
+            'uniqueidentifier' => 'char',
112
+            'xml' => 'longnvarchar',
113
+        ],
114
+    ];
115
+
116
+    // source: https://docs.oracle.com/javase/9/docs/api/java/sql/Types.html
117
+    private $valid = [
118
+        //'array' => true,
119
+        'bigint' => true,
120
+        'binary' => true,
121
+        'bit' => true,
122
+        'blob' => true,
123
+        'boolean' => true,
124
+        'char' => true,
125
+        'clob' => true,
126
+        //'datalink' => true,
127
+        'date' => true,
128
+        'decimal' => true,
129
+        'distinct' => true,
130
+        'double' => true,
131
+        'float' => true,
132
+        'integer' => true,
133
+        //'java_object' => true,
134
+        'longnvarchar' => true,
135
+        'longvarbinary' => true,
136
+        'longvarchar' => true,
137
+        'nchar' => true,
138
+        'nclob' => true,
139
+        //'null' => true,
140
+        'numeric' => true,
141
+        'nvarchar' => true,
142
+        //'other' => true,
143
+        'real' => true,
144
+        //'ref' => true,
145
+        //'ref_cursor' => true,
146
+        //'rowid' => true,
147
+        'smallint' => true,
148
+        //'sqlxml' => true,
149
+        //'struct' => true,
150
+        'time' => true,
151
+        'time_with_timezone' => true,
152
+        'timestamp' => true,
153
+        'timestamp_with_timezone' => true,
154
+        'tinyint' => true,
155
+        'varbinary' => true,
156
+        'varchar' => true,
157
+        // extra:
158
+        'geometry' => true,
159
+    ];
160
+
161
+    public function toJdbc(String $type, int $size): String
162
+    {
163
+        $jdbcType = strtolower($type);
164
+        if (isset($this->toJdbc[$this->driver]["$jdbcType($size)"])) {
165
+            $jdbcType = $this->toJdbc[$this->driver]["$jdbcType($size)"];
166
+        }
167
+        if (isset($this->toJdbc[$this->driver][$jdbcType])) {
168
+            $jdbcType = $this->toJdbc[$this->driver][$jdbcType];
169
+        }
170
+        if (isset($this->toJdbc['simplified'][$jdbcType])) {
171
+            $jdbcType = $this->toJdbc['simplified'][$jdbcType];
172
+        }
173
+        if (!isset($this->valid[$jdbcType])) {
174
+            throw new \Exception("Unsupported type '$jdbcType' for driver '$this->driver'");
175
+        }
176
+        return $jdbcType;
177
+    }
178
+
179
+    public function fromJdbc(String $type): String
180
+    {
181
+        $jdbcType = strtolower($type);
182
+        if (isset($this->fromJdbc[$this->driver][$jdbcType])) {
183
+            $jdbcType = $this->fromJdbc[$this->driver][$jdbcType];
184
+        }
185
+        return $jdbcType;
186
+    }
187
+}

+ 44
- 0
src/Tqdev/PhpCrudApi/Middleware/AuthorizationMiddleware.php View File

@@ -0,0 +1,44 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Middleware;
3
+
4
+use Tqdev\PhpCrudApi\Column\ReflectionService;
5
+use Tqdev\PhpCrudApi\Controller\Responder;
6
+use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
7
+use Tqdev\PhpCrudApi\Middleware\Router\Router;
8
+use Tqdev\PhpCrudApi\Record\ErrorCode;
9
+use Tqdev\PhpCrudApi\Request;
10
+use Tqdev\PhpCrudApi\Response;
11
+
12
+class AuthorizationMiddleware extends Middleware
13
+{
14
+    private $reflection;
15
+
16
+    public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
17
+    {
18
+        parent::__construct($router, $responder, $properties);
19
+        $this->reflection = $reflection;
20
+    }
21
+
22
+    public function handle(Request $request): Response
23
+    {
24
+        $path = $request->getPathSegment(1);
25
+        $tableName = $request->getPathSegment(2);
26
+        $database = $this->reflection->getDatabase();
27
+        if ($path == 'records' && $database->exists($tableName)) {
28
+            $table = $database->get($tableName);
29
+            $method = $request->getMethod();
30
+            $tableHandler = $this->getProperty('tableHandler', '');
31
+            if ($tableHandler !== '') {
32
+                $valid = call_user_func($handler, $method, $tableName);
33
+                if ($valid !== true && $valid !== '') {
34
+                    $details[$columnName] = $valid;
35
+                }
36
+                if (count($details) > 0) {
37
+                    return $this->responder->error(ErrorCode::INPUT_VALIDATION_FAILED, $tableName, $details);
38
+                }
39
+
40
+            }
41
+        }
42
+        return $this->next->handle($request);
43
+    }
44
+}

+ 10
- 0
src/Tqdev/PhpCrudApi/Middleware/Base/Handler.php View File

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

+ 29
- 0
src/Tqdev/PhpCrudApi/Middleware/Base/Middleware.php View File

@@ -0,0 +1,29 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Middleware\Base;
3
+
4
+use Tqdev\PhpCrudApi\Controller\Responder;
5
+use Tqdev\PhpCrudApi\Middleware\Router\Router;
6
+
7
+abstract class Middleware implements Handler
8
+{
9
+    protected $next;
10
+    protected $responder;
11
+    private $properties;
12
+
13
+    public function __construct(Router $router, Responder $responder, array $properties)
14
+    {
15
+        $router->load($this);
16
+        $this->responder = $responder;
17
+        $this->properties = $properties;
18
+    }
19
+
20
+    public function setNext(Handler $handler) /*: void*/
21
+    {
22
+        $this->next = $handler;
23
+    }
24
+
25
+    protected function getProperty(String $key, $default)
26
+    {
27
+        return isset($this->properties[$key]) ? $this->properties[$key] : $default;
28
+    }
29
+}

+ 86
- 0
src/Tqdev/PhpCrudApi/Middleware/BasicAuthMiddleware.php View File

@@ -0,0 +1,86 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Middleware;
3
+
4
+use Tqdev\PhpCrudApi\Controller\Responder;
5
+use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
6
+use Tqdev\PhpCrudApi\Record\ErrorCode;
7
+use Tqdev\PhpCrudApi\Request;
8
+use Tqdev\PhpCrudApi\Response;
9
+
10
+class BasicAuthMiddleware extends Middleware
11
+{
12
+    private function isAllowed(String $username, String $password, array &$passwords): bool
13
+    {
14
+        $hash = isset($passwords[$username]) ? $passwords[$username] : false;
15
+        if ($hash && password_verify($password, $hash)) {
16
+            if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
17
+                $passwords[$username] = password_hash($password, PASSWORD_DEFAULT);
18
+            }
19
+            return true;
20
+        }
21
+        return false;
22
+    }
23
+
24
+    private function authenticate(String $username, String $password, String $passwordFile): bool
25
+    {
26
+        if (session_status() == PHP_SESSION_NONE) {
27
+            session_start();
28
+        }
29
+        if (isset($_SESSION['user']) && $_SESSION['user'] == $username) {
30
+            return true;
31
+        }
32
+        $passwords = $this->readPasswords($passwordFile);
33
+        $allowed = $this->isAllowed($username, $password, $passwords);
34
+        if ($allowed) {
35
+            $_SESSION['user'] = $username;
36
+        }
37
+        $this->writePasswords($passwordFile, $passwords);
38
+        return $allowed;
39
+    }
40
+
41
+    private function readPasswords(String $passwordFile): array
42
+    {
43
+        $passwords = [];
44
+        $passwordLines = file($passwordFile);
45
+        foreach ($passwordLines as $passwordLine) {
46
+            if (strpos($passwordLine, ':') !== false) {
47
+                list($username, $hash) = explode(':', trim($passwordLine), 2);
48
+                if (strlen($hash) > 0 && $hash[0] != '$') {
49
+                    $hash = password_hash($hash, PASSWORD_DEFAULT);
50
+                }
51
+                $passwords[$username] = $hash;
52
+            }
53
+        }
54
+        return $passwords;
55
+    }
56
+
57
+    private function writePasswords(String $passwordFile, array $passwords): bool
58
+    {
59
+        $success = false;
60
+        $passwordFileContents = '';
61
+        foreach ($passwords as $username => $hash) {
62
+            $passwordFileContents .= "$username:$hash\n";
63
+        }
64
+        if (file_get_contents($passwordFile) != $passwordFileContents) {
65
+            $success = file_put_contents($passwordFile, $passwordFileContents) !== false;
66
+        }
67
+        return $success;
68
+    }
69
+
70
+    public function handle(Request $request): Response
71
+    {
72
+        $username = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : '';
73
+        $password = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : '';
74
+        $passwordFile = $this->getProperty('passwordFile', '.htpasswd');
75
+        if (!$username) {
76
+            $response = $this->responder->error(ErrorCode::AUTHORIZATION_REQUIRED, $username);
77
+            $realm = $this->getProperty('realm', 'Username and password required');
78
+            $response->addHeader('WWW-Authenticate', "Basic realm=\"$realm\"");
79
+        } elseif (!$this->authenticate($username, $password, $passwordFile)) {
80
+            $response = $this->responder->error(ErrorCode::ACCESS_DENIED, $username);
81
+        } else {
82
+            $response = $this->next->handle($request);
83
+        }
84
+        return $response;
85
+    }
86
+}

+ 53
- 0
src/Tqdev/PhpCrudApi/Middleware/CorsMiddleware.php View File

@@ -0,0 +1,53 @@
1
+<?php
2
+namespace Tqdev\PhpCrudApi\Middleware;
3
+
4
+use Tqdev\PhpCrudApi\Controller\Responder;
5
+use Tqdev\PhpCrudApi\Record\ErrorCode;
6
+use Tqdev\PhpCrudApi\Request;
7
+use Tqdev\PhpCrudApi\Response;
8
+use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
9
+
10
+class CorsMiddleware extends Middleware
11
+{
12
+    private function isOriginAllowed(String $origin, String $allowedOrigins): bool
13
+    {
14
+        $found = false;
15
+        foreach (explode(',', $allowedOrigins) as $allowedOrigin) {
16
+            $hostname = preg_quote(strtolower(trim($allowedOrigin)));
17
+            $regex = '/^' . str_replace('\*', '.*', $hostname) . '$/';
18
+            if (preg_match($regex, $origin)) {
19
+                $found = true;
20
+                break;
21
+            }
22
+        }
23
+        return $found;
24
+    }
25
+
26
+    public function handle(Request $request): Response
27
+    {
28
+        $method = $request->getMethod();
29
+        $origin = $request->getHeader('Origin');
30
+        $allowedOrigins = $this->getProperty('allowedOrigins', '*');
31
+        if ($origin && !$this->isOriginAllowed($origin, $allowedOrigins)) {
32
+            $response = $this->responder->error(ErrorCode::ORIGIN_FORBIDDEN, $origin);
33
+        } elseif ($method == 'OPTIONS') {
34
+            $response = new Response(Response::OK, '');
35
+            $allowHeaders = $this->getProperty('allowHeaders', 'Content-Type, X-XSRF-TOKEN');
36
+            $response->addHeader('Access-Control-Allow-Headers', $allowHeaders);
37
+            $allowMethods = $this->getProperty('allowMethods', 'OPTIONS, GET, PUT, POST, DELETE, PATCH');
38
+            $response->addHeader('Access-Control-Allow-Methods', $allowMethods);
39
+            $allowCredentials = $this->getProperty('allowCredentials', 'true');
40
+            $response->addHeader('Access-Control-Allow-Credentials', $allowCredentials);
41
+            $maxAge = $this->getProperty('maxAge', '1728000');
42
+            $response->addHeader('Access-Control-Max-Age', $maxAge);
43
+        } else {
44
+            $response = $this->next->handle($request);
45
+        }
46
+        if ($origin) {
47
+            $allowCredentials = $this->getProperty('allowCredentials', 'true');
48
+            $response->addHeader('Access-Control-Allow-Credentials', $allowCredentials);
49
+            $response->addHeader('Access-Control-Allow-Origin', $origin);
50
+        }
51
+        return $response;
52
+    }
53
+}

+ 0
- 0
src/Tqdev/PhpCrudApi/Middleware/FirewallMiddleware.php View File


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

Loading…
Cancel
Save