├── .gitignore ├── LICENSE ├── README.md ├── admin.php ├── build.php ├── composer.json ├── composer.lock ├── install.php ├── patch.php ├── screenshots └── table.png ├── src ├── Tqdev │ └── PhpCrudAdmin │ │ ├── Admin.php │ │ ├── Client │ │ ├── ApiCaller.php │ │ ├── CrudApi.php │ │ ├── CurlCaller.php │ │ └── LocalCaller.php │ │ ├── Column │ │ ├── ColumnService.php │ │ ├── DefinitionService.php │ │ └── TableService.php │ │ ├── Config.php │ │ ├── Controller │ │ ├── ColumnController.php │ │ ├── MultiResponder.php │ │ └── TableController.php │ │ ├── Document │ │ ├── CsvDocument.php │ │ └── TemplateDocument.php │ │ └── Template │ │ ├── Template.php │ │ └── TemplateString.php └── index.php ├── start.sh ├── templates ├── column │ ├── create.html │ ├── created.html │ ├── delete.html │ ├── deleted.html │ ├── list.html │ ├── update.html │ └── updated.html ├── error │ └── show.html ├── layouts │ ├── default.html │ └── error.html └── table │ ├── create.html │ ├── created.html │ ├── delete.html │ ├── deleted.html │ └── list.html └── update.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | vendor/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Maurits van der Schee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP-CRUD-ADMIN 2 | 3 | A database admin interface for MySQL, PostgreSQL or SQL Server in a single file PHP script. 4 | 5 |  6 | 7 | ## Requirements 8 | 9 | - PHP 7.0 or higher with PDO drivers for MySQL, PgSQL or SqlSrv enabled 10 | 11 | ## Installation 12 | 13 | This is a single file application! Upload "`admin.php`" somewhere and enjoy! 14 | 15 | For local development you may run PHP's built-in web server: 16 | 17 | php -S localhost:8080 18 | 19 | Test the script by opening the following URL: 20 | 21 | http://localhost:8080/admin.php/ 22 | 23 | Don't forget to modify the configuration at the bottom of the file. 24 | 25 | ## Configuration 26 | 27 | Use the 'api' config parameter to configure the embedded [PHP-CRUD-API](https://github.com/mevdschee/php-crud-api). 28 | 29 | These are the most important 'api' configuration options and their default value between brackets: 30 | 31 | - "driver": mysql, pgsql or sqlsrv (mysql) 32 | - "address": Hostname of the database server (localhost) 33 | - "port": TCP port of the database server (defaults to driver default) 34 | - "username": Username of the user connecting to the database (no default) 35 | - "password": Password of the user connecting to the database (no default) 36 | - "database": Database the connecting is made to (no default) 37 | 38 | For more information check out the [PHP-CRUD-API](https://github.com/mevdschee/php-crud-api) documentation. 39 | 40 | ## Compilation 41 | 42 | You can install all dependencies of this project using the following command: 43 | 44 | php install.php 45 | 46 | You can compile all files into a single "`ui.php`" file using: 47 | 48 | php build.php 49 | 50 | NB: The install script will patch the dependencies in the vendor directory for PHP 7.0 compatibility. 51 | 52 | ### Development 53 | 54 | You can access the non-compiled code at the URL: 55 | 56 | http://localhost:8080/src/admin/column/posts/list 57 | 58 | The non-compiled code resides in the "`src`" and "`vendor`" directories. The "`vendor`" directory contains the dependencies. 59 | 60 | ### Updating dependencies 61 | 62 | You can update all dependencies of this project using the following command: 63 | 64 | php update.php 65 | 66 | This script will install and run [Composer](https://getcomposer.org/) to update the dependencies. 67 | 68 | NB: The update script will patch the dependencies in the vendor directory for PHP 7.0 compatibility. 69 | 70 | ## Local or remote API 71 | 72 | This script is powered by [PHP-CRUD-API](https://github.com/mevdschee/php-crud-api) and embeds this project. Alternatively, it can run against a remote (live) installation. 73 | 74 | If you want to run this against a remote installation, then replace the 'api' config parameter with one called 'url' that holds the base URL of your [PHP-CRUD-API](https://github.com/mevdschee/php-crud-api) installation. 75 | -------------------------------------------------------------------------------- /build.php: -------------------------------------------------------------------------------- 1 | $entry) { 8 | if (isset($ignore[$dir . '/' . $entry])) { 9 | unset($entries[$i]); 10 | } 11 | } 12 | } 13 | 14 | function runDir(string $base, string $dir, array &$lines, array $ignore): int 15 | { 16 | $count = 0; 17 | $entries = scandir($dir); 18 | removeIgnored($dir, $entries, $ignore); 19 | sort($entries); 20 | foreach ($entries as $entry) { 21 | if ($entry === '.' || $entry === '..') { 22 | continue; 23 | } 24 | $filename = "$base/$dir/$entry"; 25 | if (is_dir($filename)) { 26 | $count += runDir($base, "$dir/$entry", $lines, $ignore); 27 | } 28 | } 29 | foreach ($entries as $entry) { 30 | $filename = "$base/$dir/$entry"; 31 | if (is_file($filename)) { 32 | if (substr($entry, -4) == '.php') { 33 | $data = file_get_contents($filename); 34 | $data = preg_replace('/\s*<\?php\s+/s', '', $data, 1); 35 | $data = preg_replace('/^.*?(vendor\/autoload|declare\s*\(\s*strict_types\s*=\s*1).*?$/m', '', $data); 36 | array_push($lines, "// file: $dir/$entry"); 37 | foreach (explode("\n", trim($data)) as $line) { 38 | if ($line) { 39 | $line = ' ' . $line; 40 | } 41 | $line = preg_replace('/^\s*(namespace[^;]+);/', '\1 {', $line); 42 | array_push($lines, $line); 43 | } 44 | array_push($lines, '}'); 45 | array_push($lines, ''); 46 | $count++; 47 | } elseif (substr($entry, -5) == '.html') { 48 | $data = file_get_contents($filename); 49 | $id = explode('.', explode('/', "$dir/$entry", 2)[1], 2)[0]; 50 | array_push($lines, "// file: $dir/$entry"); 51 | array_push($lines, 'namespace {'); 52 | array_push($lines, "\$_HTML['$id'] = <<<'END_OF_HTML'"); 53 | foreach (explode("\n", $data) as $line) { 54 | array_push($lines, $line); 55 | } 56 | array_push($lines, 'END_OF_HTML;', '}', ''); 57 | $count++; 58 | } 59 | } 60 | } 61 | return $count; 62 | } 63 | 64 | function addHeader(array &$lines) 65 | { 66 | $head = <<<'EOF' 67 | =7.0.0", 17 | "ext-json": "*", 18 | "ext-curl": "*", 19 | "psr/http-message": "*", 20 | "psr/http-factory": "*", 21 | "psr/http-server-handler": "*", 22 | "psr/http-server-middleware": "*", 23 | "nyholm/psr7": "*", 24 | "nyholm/psr7-server": "*", 25 | "mevdschee/php-crud-api": "*" 26 | }, 27 | "suggest": { 28 | }, 29 | "autoload": { 30 | "psr-4": { "Tqdev\\PhpCrudAdmin\\": "src/Tqdev/PhpCrudAdmin" } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "cfd9169765c2c7c1c93ba697c3d2b40e", 8 | "packages": [ 9 | { 10 | "name": "mevdschee/php-crud-api", 11 | "version": "v2.14.13", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/mevdschee/php-crud-api.git", 15 | "reference": "52d4ec34b2f52f5b9e0098ab675f2fbda571d424" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/mevdschee/php-crud-api/zipball/52d4ec34b2f52f5b9e0098ab675f2fbda571d424", 20 | "reference": "52d4ec34b2f52f5b9e0098ab675f2fbda571d424", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "ext-json": "*", 25 | "ext-pdo": "*", 26 | "ext-zlib": "*", 27 | "nyholm/psr7": "*", 28 | "nyholm/psr7-server": "*", 29 | "php": ">=7.0.0", 30 | "psr/http-factory": "*", 31 | "psr/http-message": "*", 32 | "psr/http-server-handler": "*", 33 | "psr/http-server-middleware": "*" 34 | }, 35 | "suggest": { 36 | "ext-memcache": "*", 37 | "ext-memcached": "*", 38 | "ext-redis": "*" 39 | }, 40 | "type": "library", 41 | "autoload": { 42 | "psr-4": { 43 | "Tqdev\\PhpCrudApi\\": "src/Tqdev/PhpCrudApi" 44 | } 45 | }, 46 | "notification-url": "https://packagist.org/downloads/", 47 | "license": [ 48 | "MIT" 49 | ], 50 | "authors": [ 51 | { 52 | "name": "Maurits van der Schee", 53 | "email": "maurits@vdschee.nl", 54 | "homepage": "https://github.com/mevdschee" 55 | } 56 | ], 57 | "description": "Single file PHP script that adds a REST API to a SQL database.", 58 | "homepage": "https://github.com/mevdschee/php-crud-api", 59 | "keywords": [ 60 | "PHP-API", 61 | "api-server", 62 | "automatic-api", 63 | "crud", 64 | "database", 65 | "geospatial", 66 | "multi-database", 67 | "mysql", 68 | "openapi", 69 | "php", 70 | "postgis", 71 | "postgresql", 72 | "rest-api", 73 | "restful", 74 | "sql-database", 75 | "sql-server", 76 | "swagger", 77 | "ubuntu-linux" 78 | ], 79 | "support": { 80 | "issues": "https://github.com/mevdschee/php-crud-api/issues", 81 | "source": "https://github.com/mevdschee/php-crud-api/tree/v2.14.13" 82 | }, 83 | "time": "2022-10-06T14:51:33+00:00" 84 | }, 85 | { 86 | "name": "nyholm/psr7", 87 | "version": "1.5.1", 88 | "source": { 89 | "type": "git", 90 | "url": "https://github.com/Nyholm/psr7.git", 91 | "reference": "f734364e38a876a23be4d906a2a089e1315be18a" 92 | }, 93 | "dist": { 94 | "type": "zip", 95 | "url": "https://api.github.com/repos/Nyholm/psr7/zipball/f734364e38a876a23be4d906a2a089e1315be18a", 96 | "reference": "f734364e38a876a23be4d906a2a089e1315be18a", 97 | "shasum": "" 98 | }, 99 | "require": { 100 | "php": ">=7.1", 101 | "php-http/message-factory": "^1.0", 102 | "psr/http-factory": "^1.0", 103 | "psr/http-message": "^1.0" 104 | }, 105 | "provide": { 106 | "psr/http-factory-implementation": "1.0", 107 | "psr/http-message-implementation": "1.0" 108 | }, 109 | "require-dev": { 110 | "http-interop/http-factory-tests": "^0.9", 111 | "php-http/psr7-integration-tests": "^1.0", 112 | "phpunit/phpunit": "^7.5 || 8.5 || 9.4", 113 | "symfony/error-handler": "^4.4" 114 | }, 115 | "type": "library", 116 | "extra": { 117 | "branch-alias": { 118 | "dev-master": "1.4-dev" 119 | } 120 | }, 121 | "autoload": { 122 | "psr-4": { 123 | "Nyholm\\Psr7\\": "src/" 124 | } 125 | }, 126 | "notification-url": "https://packagist.org/downloads/", 127 | "license": [ 128 | "MIT" 129 | ], 130 | "authors": [ 131 | { 132 | "name": "Tobias Nyholm", 133 | "email": "tobias.nyholm@gmail.com" 134 | }, 135 | { 136 | "name": "Martijn van der Ven", 137 | "email": "martijn@vanderven.se" 138 | } 139 | ], 140 | "description": "A fast PHP7 implementation of PSR-7", 141 | "homepage": "https://tnyholm.se", 142 | "keywords": [ 143 | "psr-17", 144 | "psr-7" 145 | ], 146 | "support": { 147 | "issues": "https://github.com/Nyholm/psr7/issues", 148 | "source": "https://github.com/Nyholm/psr7/tree/1.5.1" 149 | }, 150 | "funding": [ 151 | { 152 | "url": "https://github.com/Zegnat", 153 | "type": "github" 154 | }, 155 | { 156 | "url": "https://github.com/nyholm", 157 | "type": "github" 158 | } 159 | ], 160 | "time": "2022-06-22T07:13:36+00:00" 161 | }, 162 | { 163 | "name": "nyholm/psr7-server", 164 | "version": "1.0.2", 165 | "source": { 166 | "type": "git", 167 | "url": "https://github.com/Nyholm/psr7-server.git", 168 | "reference": "b846a689844cef114e8079d8c80f0afd96745ae3" 169 | }, 170 | "dist": { 171 | "type": "zip", 172 | "url": "https://api.github.com/repos/Nyholm/psr7-server/zipball/b846a689844cef114e8079d8c80f0afd96745ae3", 173 | "reference": "b846a689844cef114e8079d8c80f0afd96745ae3", 174 | "shasum": "" 175 | }, 176 | "require": { 177 | "php": "^7.1 || ^8.0", 178 | "psr/http-factory": "^1.0", 179 | "psr/http-message": "^1.0" 180 | }, 181 | "require-dev": { 182 | "nyholm/nsa": "^1.1", 183 | "nyholm/psr7": "^1.3", 184 | "phpunit/phpunit": "^7.0 || ^8.5 || ^9.3" 185 | }, 186 | "type": "library", 187 | "autoload": { 188 | "psr-4": { 189 | "Nyholm\\Psr7Server\\": "src/" 190 | } 191 | }, 192 | "notification-url": "https://packagist.org/downloads/", 193 | "license": [ 194 | "MIT" 195 | ], 196 | "authors": [ 197 | { 198 | "name": "Tobias Nyholm", 199 | "email": "tobias.nyholm@gmail.com" 200 | }, 201 | { 202 | "name": "Martijn van der Ven", 203 | "email": "martijn@vanderven.se" 204 | } 205 | ], 206 | "description": "Helper classes to handle PSR-7 server requests", 207 | "homepage": "http://tnyholm.se", 208 | "keywords": [ 209 | "psr-17", 210 | "psr-7" 211 | ], 212 | "support": { 213 | "issues": "https://github.com/Nyholm/psr7-server/issues", 214 | "source": "https://github.com/Nyholm/psr7-server/tree/1.0.2" 215 | }, 216 | "funding": [ 217 | { 218 | "url": "https://github.com/Zegnat", 219 | "type": "github" 220 | }, 221 | { 222 | "url": "https://github.com/nyholm", 223 | "type": "github" 224 | } 225 | ], 226 | "time": "2021-05-12T11:11:27+00:00" 227 | }, 228 | { 229 | "name": "php-http/message-factory", 230 | "version": "v1.0.2", 231 | "source": { 232 | "type": "git", 233 | "url": "https://github.com/php-http/message-factory.git", 234 | "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1" 235 | }, 236 | "dist": { 237 | "type": "zip", 238 | "url": "https://api.github.com/repos/php-http/message-factory/zipball/a478cb11f66a6ac48d8954216cfed9aa06a501a1", 239 | "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1", 240 | "shasum": "" 241 | }, 242 | "require": { 243 | "php": ">=5.4", 244 | "psr/http-message": "^1.0" 245 | }, 246 | "type": "library", 247 | "extra": { 248 | "branch-alias": { 249 | "dev-master": "1.0-dev" 250 | } 251 | }, 252 | "autoload": { 253 | "psr-4": { 254 | "Http\\Message\\": "src/" 255 | } 256 | }, 257 | "notification-url": "https://packagist.org/downloads/", 258 | "license": [ 259 | "MIT" 260 | ], 261 | "authors": [ 262 | { 263 | "name": "Márk Sági-Kazár", 264 | "email": "mark.sagikazar@gmail.com" 265 | } 266 | ], 267 | "description": "Factory interfaces for PSR-7 HTTP Message", 268 | "homepage": "http://php-http.org", 269 | "keywords": [ 270 | "factory", 271 | "http", 272 | "message", 273 | "stream", 274 | "uri" 275 | ], 276 | "support": { 277 | "issues": "https://github.com/php-http/message-factory/issues", 278 | "source": "https://github.com/php-http/message-factory/tree/master" 279 | }, 280 | "time": "2015-12-19T14:08:53+00:00" 281 | }, 282 | { 283 | "name": "psr/http-factory", 284 | "version": "1.0.1", 285 | "source": { 286 | "type": "git", 287 | "url": "https://github.com/php-fig/http-factory.git", 288 | "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" 289 | }, 290 | "dist": { 291 | "type": "zip", 292 | "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", 293 | "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", 294 | "shasum": "" 295 | }, 296 | "require": { 297 | "php": ">=7.0.0", 298 | "psr/http-message": "^1.0" 299 | }, 300 | "type": "library", 301 | "extra": { 302 | "branch-alias": { 303 | "dev-master": "1.0.x-dev" 304 | } 305 | }, 306 | "autoload": { 307 | "psr-4": { 308 | "Psr\\Http\\Message\\": "src/" 309 | } 310 | }, 311 | "notification-url": "https://packagist.org/downloads/", 312 | "license": [ 313 | "MIT" 314 | ], 315 | "authors": [ 316 | { 317 | "name": "PHP-FIG", 318 | "homepage": "http://www.php-fig.org/" 319 | } 320 | ], 321 | "description": "Common interfaces for PSR-7 HTTP message factories", 322 | "keywords": [ 323 | "factory", 324 | "http", 325 | "message", 326 | "psr", 327 | "psr-17", 328 | "psr-7", 329 | "request", 330 | "response" 331 | ], 332 | "support": { 333 | "source": "https://github.com/php-fig/http-factory/tree/master" 334 | }, 335 | "time": "2019-04-30T12:38:16+00:00" 336 | }, 337 | { 338 | "name": "psr/http-message", 339 | "version": "1.0.1", 340 | "source": { 341 | "type": "git", 342 | "url": "https://github.com/php-fig/http-message.git", 343 | "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" 344 | }, 345 | "dist": { 346 | "type": "zip", 347 | "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", 348 | "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", 349 | "shasum": "" 350 | }, 351 | "require": { 352 | "php": ">=5.3.0" 353 | }, 354 | "type": "library", 355 | "extra": { 356 | "branch-alias": { 357 | "dev-master": "1.0.x-dev" 358 | } 359 | }, 360 | "autoload": { 361 | "psr-4": { 362 | "Psr\\Http\\Message\\": "src/" 363 | } 364 | }, 365 | "notification-url": "https://packagist.org/downloads/", 366 | "license": [ 367 | "MIT" 368 | ], 369 | "authors": [ 370 | { 371 | "name": "PHP-FIG", 372 | "homepage": "http://www.php-fig.org/" 373 | } 374 | ], 375 | "description": "Common interface for HTTP messages", 376 | "homepage": "https://github.com/php-fig/http-message", 377 | "keywords": [ 378 | "http", 379 | "http-message", 380 | "psr", 381 | "psr-7", 382 | "request", 383 | "response" 384 | ], 385 | "support": { 386 | "source": "https://github.com/php-fig/http-message/tree/master" 387 | }, 388 | "time": "2016-08-06T14:39:51+00:00" 389 | }, 390 | { 391 | "name": "psr/http-server-handler", 392 | "version": "1.0.1", 393 | "source": { 394 | "type": "git", 395 | "url": "https://github.com/php-fig/http-server-handler.git", 396 | "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7" 397 | }, 398 | "dist": { 399 | "type": "zip", 400 | "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/aff2f80e33b7f026ec96bb42f63242dc50ffcae7", 401 | "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7", 402 | "shasum": "" 403 | }, 404 | "require": { 405 | "php": ">=7.0", 406 | "psr/http-message": "^1.0" 407 | }, 408 | "type": "library", 409 | "extra": { 410 | "branch-alias": { 411 | "dev-master": "1.0.x-dev" 412 | } 413 | }, 414 | "autoload": { 415 | "psr-4": { 416 | "Psr\\Http\\Server\\": "src/" 417 | } 418 | }, 419 | "notification-url": "https://packagist.org/downloads/", 420 | "license": [ 421 | "MIT" 422 | ], 423 | "authors": [ 424 | { 425 | "name": "PHP-FIG", 426 | "homepage": "http://www.php-fig.org/" 427 | } 428 | ], 429 | "description": "Common interface for HTTP server-side request handler", 430 | "keywords": [ 431 | "handler", 432 | "http", 433 | "http-interop", 434 | "psr", 435 | "psr-15", 436 | "psr-7", 437 | "request", 438 | "response", 439 | "server" 440 | ], 441 | "support": { 442 | "issues": "https://github.com/php-fig/http-server-handler/issues", 443 | "source": "https://github.com/php-fig/http-server-handler/tree/master" 444 | }, 445 | "time": "2018-10-30T16:46:14+00:00" 446 | }, 447 | { 448 | "name": "psr/http-server-middleware", 449 | "version": "1.0.1", 450 | "source": { 451 | "type": "git", 452 | "url": "https://github.com/php-fig/http-server-middleware.git", 453 | "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5" 454 | }, 455 | "dist": { 456 | "type": "zip", 457 | "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/2296f45510945530b9dceb8bcedb5cb84d40c5f5", 458 | "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5", 459 | "shasum": "" 460 | }, 461 | "require": { 462 | "php": ">=7.0", 463 | "psr/http-message": "^1.0", 464 | "psr/http-server-handler": "^1.0" 465 | }, 466 | "type": "library", 467 | "extra": { 468 | "branch-alias": { 469 | "dev-master": "1.0.x-dev" 470 | } 471 | }, 472 | "autoload": { 473 | "psr-4": { 474 | "Psr\\Http\\Server\\": "src/" 475 | } 476 | }, 477 | "notification-url": "https://packagist.org/downloads/", 478 | "license": [ 479 | "MIT" 480 | ], 481 | "authors": [ 482 | { 483 | "name": "PHP-FIG", 484 | "homepage": "http://www.php-fig.org/" 485 | } 486 | ], 487 | "description": "Common interface for HTTP server-side middleware", 488 | "keywords": [ 489 | "http", 490 | "http-interop", 491 | "middleware", 492 | "psr", 493 | "psr-15", 494 | "psr-7", 495 | "request", 496 | "response" 497 | ], 498 | "support": { 499 | "issues": "https://github.com/php-fig/http-server-middleware/issues", 500 | "source": "https://github.com/php-fig/http-server-middleware/tree/master" 501 | }, 502 | "time": "2018-10-30T17:12:04+00:00" 503 | } 504 | ], 505 | "packages-dev": [], 506 | "aliases": [], 507 | "minimum-stability": "stable", 508 | "stability-flags": [], 509 | "prefer-stable": false, 510 | "prefer-lowest": false, 511 | "platform": { 512 | "php": ">=7.0.0", 513 | "ext-json": "*", 514 | "ext-curl": "*" 515 | }, 516 | "platform-dev": [], 517 | "plugin-api-version": "2.3.0" 518 | } 519 | -------------------------------------------------------------------------------- /install.php: -------------------------------------------------------------------------------- 1 | getApi()); 31 | if ($config->getUrl()) { 32 | $caller = new CurlCaller($config->getUrl()); 33 | } 34 | $api = new CrudApi($caller); 35 | $prefix = sprintf('PhpCrudAdmin-%s-%s-', substr(md5($config->getUrl()), 0, 12), substr(md5(__FILE__), 0, 12)); 36 | $cache = CacheFactory::create($config->getCacheType(), $prefix, $config->getCachePath()); 37 | $definition = new DefinitionService($api); 38 | $responder = new MultiResponder($config->getTemplatePath()); 39 | $router = new SimpleRouter($config->getBasePath(), $responder, $cache, $config->getCacheTime(), $config->getDebug()); 40 | $responder->setVariable('base', $router->getBasePath()); 41 | $responder->setVariable('menu', $definition->getTableNames()); 42 | $responder->setVariable('table', ''); 43 | foreach ($config->getControllers() as $controller) { 44 | switch ($controller) { 45 | case 'columns': 46 | $columns = new ColumnService($api, $definition); 47 | new ColumnController($router, $responder, $columns); 48 | break; 49 | case 'tables': 50 | $tables = new TableService($api, $definition); 51 | $controller = new TableController($router, $responder, $tables); 52 | $router->register('GET', '/', array($controller, '_list')); 53 | break; 54 | } 55 | } 56 | $this->router = $router; 57 | $this->responder = $responder; 58 | $this->debug = $config->getDebug(); 59 | } 60 | 61 | private function addParsedBody(ServerRequestInterface $request): ServerRequestInterface 62 | { 63 | $body = $request->getBody(); 64 | if ($body->isReadable() && $body->isSeekable()) { 65 | $contents = $body->getContents(); 66 | $body->rewind(); 67 | if ($contents) { 68 | parse_str($contents, $parsedBody); 69 | $request = $request->withParsedBody($parsedBody); 70 | } 71 | } 72 | return $request; 73 | } 74 | 75 | public function handle(ServerRequestInterface $request): ResponseInterface 76 | { 77 | $response = null; 78 | try { 79 | $response = $this->router->route($this->addParsedBody($request)); 80 | } catch (\Throwable $e) { 81 | $response = $this->responder->error(ErrorCode::ERROR_NOT_FOUND, $e->getMessage()); 82 | if ($this->debug) { 83 | $response = ResponseUtils::addExceptionHeaders($response, $e); 84 | } 85 | } 86 | return $response; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudAdmin/Client/ApiCaller.php: -------------------------------------------------------------------------------- 1 | caller = $caller; 12 | } 13 | 14 | public function readDatabase(array $args) 15 | { 16 | return $this->caller->call('GET', '/columns', $args); 17 | } 18 | 19 | public function readTable(string $table, array $args) 20 | { 21 | return $this->caller->call('GET', '/columns/' . rawurlencode($table), $args); 22 | } 23 | 24 | public function readColumn(string $table, string $column, array $args) 25 | { 26 | return $this->caller->call('GET', '/columns/' . rawurlencode($table) . '/' . rawurlencode($column), $args); 27 | } 28 | 29 | public function updateColumn(string $table, string $column, array $data) 30 | { 31 | return $this->caller->call('PUT', '/columns/' . rawurlencode($table) . '/' . rawurlencode($column), [], $data); 32 | } 33 | 34 | public function createColumn(string $table, array $data) 35 | { 36 | return $this->caller->call('POST', '/columns/' . rawurlencode($table), [], $data); 37 | } 38 | 39 | public function deleteColumn(string $table, string $column) 40 | { 41 | return $this->caller->call('DELETE', '/columns/' . rawurlencode($table) . '/' . rawurlencode($column), []); 42 | } 43 | 44 | public function createTable(array $data) 45 | { 46 | return $this->caller->call('POST', '/columns', [], $data); 47 | } 48 | 49 | public function deleteTable(string $table) 50 | { 51 | return $this->caller->call('DELETE', '/columns/' . rawurlencode($table), []); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudAdmin/Client/CurlCaller.php: -------------------------------------------------------------------------------- 1 | url = $url; 12 | } 13 | 14 | public function call(string $method, string $path, array $args = [], $data = false) 15 | { 16 | $query = rtrim('?' . preg_replace('|%5B[0-9]+%5D|', '', http_build_query($args)), '?'); 17 | $ch = curl_init(); 18 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); 19 | curl_setopt($ch, CURLOPT_URL, $this->url . $path . $query); 20 | if ($data) { 21 | $content = json_encode($data); 22 | curl_setopt($ch, CURLOPT_POSTFIELDS, $content); 23 | $headers = array(); 24 | $headers[] = 'Content-Type: application/json'; 25 | $headers[] = 'Content-Length: ' . strlen($content); 26 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 27 | } 28 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 29 | $response = curl_exec($ch); 30 | curl_close($ch); 31 | return json_decode($response, true); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudAdmin/Client/LocalCaller.php: -------------------------------------------------------------------------------- 1 | config = $config; 16 | } 17 | 18 | public function call(string $method, string $path, array $args = [], $data = false) 19 | { 20 | $query = rtrim('?' . preg_replace('|%5B[0-9]+%5D|', '', http_build_query($args)), '?'); 21 | $config = new Config($this->config); 22 | $body = ''; 23 | if ($data !== false) { 24 | $body = json_encode($data); 25 | } 26 | $request = RequestFactory::fromString(trim("$method $path$query\n\n$body")); 27 | $api = new Api($config); 28 | $response = $api->handle($request); 29 | return json_decode($response->getBody(), true); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudAdmin/Column/ColumnService.php: -------------------------------------------------------------------------------- 1 | api = $api; 17 | $this->definition = $definition; 18 | } 19 | 20 | public function hasTable(string $table, string $action): bool 21 | { 22 | return $this->definition->hasTable($table, $action); 23 | } 24 | 25 | private function getDataTypeValues(): array 26 | { 27 | $types = array('bigint', 'bit', 'blob', 'boolean', 'clob', 'date', 'decimal', 'double', 'float', 'geometry', 'integer', 'time', 'timestamp', 'varbinary', 'varchar'); 28 | return array_combine($types, $types); 29 | } 30 | 31 | private function getTableNameValues(): array 32 | { 33 | $tables = $this->definition->getTableNames(); 34 | return array_combine($tables, $tables); 35 | } 36 | 37 | private function getBooleanValues(): array 38 | { 39 | return array('no', 'yes'); 40 | } 41 | 42 | private function makeForm(array $column): array 43 | { 44 | $form = array(); 45 | foreach ($column as $key => $value) { 46 | $form[$key] = array('value' => $value, 'type' => 'text', 'required' => false); 47 | switch ($key) { 48 | case 'name': 49 | $form[$key]['required'] = true; 50 | break; 51 | case 'type': 52 | $form[$key]['required'] = true; 53 | $form[$key]['type'] = 'select'; 54 | $form[$key]['values'] = $this->getDataTypeValues(); 55 | break; 56 | case 'length': 57 | case 'precision': 58 | case 'scale': 59 | $form[$key]['type'] = 'number'; 60 | break; 61 | case 'nullable': 62 | case 'pk': 63 | $form[$key]['required'] = true; 64 | $form[$key]['type'] = 'select'; 65 | $form[$key]['values'] = $this->getBooleanValues(); 66 | break; 67 | case 'fk': 68 | $form[$key]['type'] = 'select'; 69 | $form[$key]['values'] = $this->getTableNameValues(); 70 | break; 71 | } 72 | } 73 | return $form; 74 | } 75 | 76 | public function createForm(string $table, string $action): TemplateDocument 77 | { 78 | $column = $this->definition->getNewColumn(); 79 | 80 | $form = $this->makeForm($column); 81 | 82 | $variables = array( 83 | 'table' => $table, 84 | 'action' => $action, 85 | 'form' => $form, 86 | ); 87 | 88 | return new TemplateDocument('layouts/default', 'column/create', $variables); 89 | } 90 | 91 | public function create(string $table, string $action, /* object */ $column): TemplateDocument 92 | { 93 | $success = $this->api->createColumn($table, $column); 94 | 95 | $variables = array( 96 | 'table' => $table, 97 | 'action' => $action, 98 | 'name' => $column['name'], 99 | 'success' => $success, 100 | ); 101 | 102 | return new TemplateDocument('layouts/default', 'column/created', $variables); 103 | } 104 | 105 | public function updateForm(string $table, string $action, string $name): TemplateDocument 106 | { 107 | $column = $this->definition->getColumn($table, $name); 108 | 109 | $form = $this->makeForm($column); 110 | 111 | $variables = array( 112 | 'table' => $table, 113 | 'action' => $action, 114 | 'name' => $column['name'], 115 | 'form' => $form, 116 | ); 117 | 118 | return new TemplateDocument('layouts/default', 'column/update', $variables); 119 | } 120 | 121 | public function update(string $table, string $action, string $name, /* object */ $column): TemplateDocument 122 | { 123 | $success = $this->api->updateColumn($table, $name, $column); 124 | 125 | $variables = array( 126 | 'table' => $table, 127 | 'action' => $action, 128 | 'name' => $column['name'] ?: $name, 129 | 'success' => $success, 130 | ); 131 | 132 | return new TemplateDocument('layouts/default', 'column/updated', $variables); 133 | } 134 | 135 | public function deleteForm(string $table, string $action, string $name): TemplateDocument 136 | { 137 | $column = $this->definition->getColumn($table, $name); 138 | 139 | $variables = array( 140 | 'table' => $table, 141 | 'action' => $action, 142 | 'name' => $column['name'], 143 | ); 144 | 145 | return new TemplateDocument('layouts/default', 'column/delete', $variables); 146 | } 147 | 148 | public function delete(string $table, string $action, string $name): TemplateDocument 149 | { 150 | $success = $this->api->deleteColumn($table, $name); 151 | 152 | $variables = array( 153 | 'table' => $table, 154 | 'action' => $action, 155 | 'name' => $name, 156 | 'success' => $success, 157 | ); 158 | 159 | return new TemplateDocument('layouts/default', 'column/deleted', $variables); 160 | } 161 | 162 | public function _list(string $table, string $action): TemplateDocument 163 | { 164 | $data = $this->definition->getTable($table); 165 | 166 | $variables = array( 167 | 'table' => $table, 168 | 'action' => $action, 169 | 'columns' => $data['columns'], 170 | ); 171 | 172 | return new TemplateDocument('layouts/default', 'column/list', $variables); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudAdmin/Column/DefinitionService.php: -------------------------------------------------------------------------------- 1 | api = $api; 15 | $this->database = $this->optimizeDatabase($this->api->readDatabase(array())); 16 | $this->properties = array(); 17 | } 18 | 19 | private function getColumnFields(): array 20 | { 21 | return array('name', 'type', 'length', 'precision', 'scale', 'nullable', 'pk', 'fk'); 22 | } 23 | 24 | private function fillSparse(array $array): array 25 | { 26 | $full = array(); 27 | $keys = $this->getColumnFields(); 28 | foreach ($keys as $key) { 29 | if (!key_exists($key, $array)) { 30 | $full[$key] = null; 31 | } else { 32 | $full[$key] = $array[$key]; 33 | } 34 | } 35 | return $full; 36 | } 37 | 38 | private function fillAllSparse(array $array): array 39 | { 40 | $full = array(); 41 | foreach ($array as $key => $value) { 42 | $full[$key] = $this->fillSparse($value); 43 | } 44 | return $full; 45 | } 46 | 47 | private function optimizeDatabase($database) 48 | { 49 | $database['tables'] = $this->makeNamed($database['tables']); 50 | foreach ($database['tables'] as $name => $table) { 51 | $database['tables'][$name]['columns'] = $this->fillAllSparse($this->makeNamed($table['columns'])); 52 | } 53 | return $database; 54 | } 55 | 56 | private function makeNamed(array $array) 57 | { 58 | $named = array(); 59 | foreach ($array as $item) { 60 | foreach ($item as $key => $value) { 61 | if ($key == 'name') { 62 | $named[$value] = $item; 63 | break; 64 | } 65 | } 66 | } 67 | return $named; 68 | } 69 | 70 | public function hasTable(string $tableName): bool 71 | { 72 | return isset($this->database['tables'][$tableName]); 73 | } 74 | 75 | public function getTable(string $tableName) 76 | { 77 | return $this->database['tables'][$tableName]; 78 | } 79 | 80 | public function getNewColumn() 81 | { 82 | return array_fill_keys($this->getColumnFields(), null); 83 | } 84 | 85 | public function getColumn(string $tableName, string $columnName) 86 | { 87 | return $this->database['tables'][$tableName]['columns'][$columnName]; 88 | } 89 | 90 | public function getTableNames() 91 | { 92 | return array_keys(array_filter($this->database['tables'], function ($table) { 93 | return $table['type'] == 'table'; 94 | })); 95 | } 96 | 97 | public function getViewNames() 98 | { 99 | return array_keys(array_filter($this->database['tables'], function ($table) { 100 | return $table['type'] == 'view'; 101 | })); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudAdmin/Column/TableService.php: -------------------------------------------------------------------------------- 1 | api = $api; 17 | $this->definition = $definition; 18 | } 19 | 20 | public function hasTable(string $action): bool 21 | { 22 | return $this->definition->hasTable($action); 23 | } 24 | 25 | private function makeForm(array $table): array 26 | { 27 | $form = array(); 28 | foreach ($table as $key => $value) { 29 | $form[$key] = array('value' => $value, 'type' => 'text', 'required' => false); 30 | switch ($key) { 31 | case 'name': 32 | $form[$key]['required'] = true; 33 | break; 34 | } 35 | } 36 | return $form; 37 | } 38 | 39 | public function createForm(string $action): TemplateDocument 40 | { 41 | $table = array('name' => ''); 42 | 43 | $form = $this->makeForm($table); 44 | 45 | $variables = array( 46 | 'action' => $action, 47 | 'form' => $form, 48 | ); 49 | 50 | return new TemplateDocument('layouts/default', 'table/create', $variables); 51 | } 52 | 53 | public function create(string $action, /* object */ $table): TemplateDocument 54 | { 55 | $success = $this->api->createTable($table); 56 | 57 | $variables = array( 58 | 'action' => $action, 59 | 'name' => $table['name'], 60 | 'success' => $success, 61 | ); 62 | 63 | return new TemplateDocument('layouts/default', 'table/created', $variables); 64 | } 65 | 66 | public function deleteForm(string $action, string $name): TemplateDocument 67 | { 68 | $table = $this->definition->getTable($name); 69 | 70 | $variables = array( 71 | 'action' => $action, 72 | 'table' => $table['name'], 73 | ); 74 | 75 | return new TemplateDocument('layouts/default', 'table/delete', $variables); 76 | } 77 | 78 | public function delete(string $action, string $name): TemplateDocument 79 | { 80 | $success = $this->api->deleteTable($name); 81 | 82 | $variables = array( 83 | 'action' => $action, 84 | 'table' => $name, 85 | 'success' => $success, 86 | ); 87 | 88 | return new TemplateDocument('layouts/default', 'table/deleted', $variables); 89 | } 90 | 91 | public function _list(string $action): TemplateDocument 92 | { 93 | $tables = $this->definition->getTableNames(); 94 | 95 | $variables = array( 96 | 'action' => $action, 97 | 'tables' => $tables, 98 | ); 99 | 100 | return new TemplateDocument('layouts/default', 'table/list', $variables); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudAdmin/Config.php: -------------------------------------------------------------------------------- 1 | '', 11 | 'api' => [], 12 | 'definition' => '', 13 | 'controllers' => 'columns,tables', 14 | 'cacheType' => 'TempFile', 15 | 'cachePath' => '', 16 | 'cacheTime' => 10, 17 | 'debug' => false, 18 | 'basePath' => '', 19 | 'templatePath' => '.', 20 | ]; 21 | 22 | public function __construct(array $values) 23 | { 24 | $newValues = array_merge($this->values, $values); 25 | $diff = array_diff_key($newValues, $this->values); 26 | if (!empty($diff)) { 27 | $key = array_keys($diff)[0]; 28 | throw new \Exception("Config has invalid value '$key'"); 29 | } 30 | $this->values = $newValues; 31 | } 32 | 33 | private function getEnvironmentVariableName(string $key): string 34 | { 35 | $prefix = "PHP_CRUD_ADMIN_"; 36 | $suffix = strtoupper(preg_replace('/(?values[$key] ?? $default; 44 | } 45 | $variableName = $this->getEnvironmentVariableName($key); 46 | return getenv($variableName, true) ?: ($this->values[$key] ?? $default); 47 | } 48 | 49 | public function getMiddlewares(): array 50 | { 51 | return []; 52 | } 53 | 54 | public function getControllers(): array 55 | { 56 | return array_map('trim', explode(',', $this->values['controllers'])); 57 | } 58 | 59 | public function getUrl(): string 60 | { 61 | return $this->values['url']; 62 | } 63 | 64 | public function getApi(): array 65 | { 66 | return $this->values['api']; 67 | } 68 | 69 | public function getDefinition(): string 70 | { 71 | return $this->values['definition']; 72 | } 73 | 74 | public function getCacheType(): string 75 | { 76 | return $this->values['cacheType']; 77 | } 78 | 79 | public function getCachePath(): string 80 | { 81 | return $this->values['cachePath']; 82 | } 83 | 84 | public function getCacheTime(): int 85 | { 86 | return $this->values['cacheTime']; 87 | } 88 | 89 | public function getDebug(): bool 90 | { 91 | return $this->values['debug']; 92 | } 93 | 94 | public function getBasePath(): string 95 | { 96 | return $this->values['basePath']; 97 | } 98 | 99 | public function getTemplatePath(): string 100 | { 101 | return $this->values['templatePath']; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudAdmin/Controller/ColumnController.php: -------------------------------------------------------------------------------- 1 | register('GET', '/admin/column/*/create', array($this, 'createForm')); 21 | $router->register('POST', '/admin/column/*/create', array($this, 'create')); 22 | $router->register('GET', '/admin/column/*/update/*', array($this, 'updateForm')); 23 | $router->register('POST', '/admin/column/*/update/*', array($this, 'update')); 24 | $router->register('GET', '/admin/column/*/delete/*', array($this, 'deleteForm')); 25 | $router->register('POST', '/admin/column/*/delete/*', array($this, 'delete')); 26 | $router->register('GET', '/admin/column/*/list', array($this, '_list')); 27 | $this->service = $service; 28 | $this->responder = $responder; 29 | } 30 | 31 | public function createForm(ServerRequestInterface $request): ResponseInterface 32 | { 33 | $table = RequestUtils::getPathSegment($request, 3); 34 | $action = RequestUtils::getPathSegment($request, 4); 35 | if (!$this->service->hasTable($table, $action)) { 36 | return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table); 37 | } 38 | $result = $this->service->createForm($table, $action); 39 | return $this->responder->success($result); 40 | } 41 | 42 | public function create(ServerRequestInterface $request): ResponseInterface 43 | { 44 | $table = RequestUtils::getPathSegment($request, 3); 45 | $action = RequestUtils::getPathSegment($request, 4); 46 | if (!$this->service->hasTable($table, $action)) { 47 | return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table); 48 | } 49 | $column = $request->getParsedBody(); 50 | if ($column === null) { 51 | return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, ''); 52 | } 53 | $result = $this->service->create($table, $action, $column); 54 | return $this->responder->success($result); 55 | } 56 | 57 | public function updateForm(ServerRequestInterface $request): ResponseInterface 58 | { 59 | $table = RequestUtils::getPathSegment($request, 3); 60 | $action = RequestUtils::getPathSegment($request, 4); 61 | $name = RequestUtils::getPathSegment($request, 5); 62 | if (!$this->service->hasTable($table, $action)) { 63 | return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table); 64 | } 65 | $result = $this->service->updateForm($table, $action, $name); 66 | return $this->responder->success($result); 67 | } 68 | 69 | public function update(ServerRequestInterface $request): ResponseInterface 70 | { 71 | $table = RequestUtils::getPathSegment($request, 3); 72 | $action = RequestUtils::getPathSegment($request, 4); 73 | $name = RequestUtils::getPathSegment($request, 5); 74 | if (!$this->service->hasTable($table, $action)) { 75 | return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table); 76 | } 77 | $column = $request->getParsedBody(); 78 | if ($column === null) { 79 | return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, ''); 80 | } 81 | $result = $this->service->update($table, $action, $name, $column); 82 | return $this->responder->success($result); 83 | } 84 | 85 | public function deleteForm(ServerRequestInterface $request): ResponseInterface 86 | { 87 | $table = RequestUtils::getPathSegment($request, 3); 88 | $action = RequestUtils::getPathSegment($request, 4); 89 | $name = RequestUtils::getPathSegment($request, 5); 90 | if (!$this->service->hasTable($table, 'read')) { 91 | return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table); 92 | } 93 | $result = $this->service->deleteForm($table, $action, $name); 94 | return $this->responder->success($result); 95 | } 96 | 97 | public function delete(ServerRequestInterface $request): ResponseInterface 98 | { 99 | $table = RequestUtils::getPathSegment($request, 3); 100 | $action = RequestUtils::getPathSegment($request, 4); 101 | $name = RequestUtils::getPathSegment($request, 5); 102 | if (!$this->service->hasTable($table, 'read')) { 103 | return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table); 104 | } 105 | $result = $this->service->delete($table, $action, $name); 106 | return $this->responder->success($result); 107 | } 108 | 109 | public function _list(ServerRequestInterface $request): ResponseInterface 110 | { 111 | $table = RequestUtils::getPathSegment($request, 3); 112 | $action = RequestUtils::getPathSegment($request, 4); 113 | if (!$this->service->hasTable($table, $action)) { 114 | return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table); 115 | } 116 | $result = $this->service->_list($table, $action); 117 | return $this->responder->success($result); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudAdmin/Controller/MultiResponder.php: -------------------------------------------------------------------------------- 1 | variables = array(); 21 | $this->templatePath = $templatePath; 22 | } 23 | 24 | public function setVariable(string $name, $value) 25 | { 26 | $this->variables[$name] = $value; 27 | } 28 | 29 | public function error(int $error, string $argument, $details = null): ResponseInterface 30 | { 31 | $errorCode = new ErrorCode($error); 32 | $status = $errorCode->getStatus(); 33 | $document = new ErrorDocument($errorCode, $argument, $details); 34 | $result = new TemplateDocument('layouts/error', 'error/show', $document->serialize()); 35 | $result->addVariables($this->variables); 36 | $result->setTemplatePath($this->templatePath); 37 | return ResponseFactory::fromHtml($status, (string) $result); 38 | } 39 | 40 | public function success($result): ResponseInterface 41 | { 42 | if ($result instanceof CsvDocument) { 43 | return ResponseFactory::fromCsv(ResponseFactory::OK, (string) $result); 44 | } elseif ($result instanceof TemplateDocument) { 45 | $result->addVariables($this->variables); 46 | $result->setTemplatePath($this->templatePath); 47 | return ResponseFactory::fromHtml(ResponseFactory::OK, (string) $result); 48 | } else { 49 | throw new \Exception('Document type not supported: ' . get_class($result)); 50 | } 51 | } 52 | 53 | public function multi($results): ResponseInterface 54 | { 55 | return ResponseFactory::fromHtml(ResponseFactory::OK, (string) "Not supported"); 56 | } 57 | 58 | public function exception($exception): ResponseInterface 59 | { 60 | return ResponseFactory::fromHtml(ResponseFactory::INTERNAL_SERVER_ERROR, (string) $exception); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudAdmin/Controller/TableController.php: -------------------------------------------------------------------------------- 1 | register('GET', '/admin/table/create', array($this, 'createForm')); 21 | $router->register('POST', '/admin/table/create', array($this, 'create')); 22 | $router->register('GET', '/admin/table/delete/*', array($this, 'deleteForm')); 23 | $router->register('POST', '/admin/table/delete/*', array($this, 'delete')); 24 | $router->register('GET', '/admin/table/list', array($this, '_list')); 25 | $this->service = $service; 26 | $this->responder = $responder; 27 | } 28 | 29 | public function createForm(ServerRequestInterface $request): ResponseInterface 30 | { 31 | $action = RequestUtils::getPathSegment($request, 3); 32 | $result = $this->service->createForm($action); 33 | return $this->responder->success($result); 34 | } 35 | 36 | public function create(ServerRequestInterface $request): ResponseInterface 37 | { 38 | $action = RequestUtils::getPathSegment($request, 3); 39 | $table = $request->getParsedBody(); 40 | if ($table === null) { 41 | return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, ''); 42 | } 43 | $table['columns'] = [["name" => "id", "type" => "bigint", "pk" => true]]; 44 | $result = $this->service->create($action, $table); 45 | return $this->responder->success($result); 46 | } 47 | 48 | public function deleteForm(ServerRequestInterface $request): ResponseInterface 49 | { 50 | $action = RequestUtils::getPathSegment($request, 3); 51 | $name = RequestUtils::getPathSegment($request, 4); 52 | if (!$this->service->hasTable($name, 'read')) { 53 | return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $name); 54 | } 55 | $result = $this->service->deleteForm($action, $name); 56 | return $this->responder->success($result); 57 | } 58 | 59 | public function delete(ServerRequestInterface $request): ResponseInterface 60 | { 61 | $action = RequestUtils::getPathSegment($request, 3); 62 | $name = RequestUtils::getPathSegment($request, 4); 63 | if (!$this->service->hasTable($name, 'read')) { 64 | return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $name); 65 | } 66 | $result = $this->service->delete($action, $name); 67 | return $this->responder->success($result); 68 | } 69 | 70 | public function _list(ServerRequestInterface $request): ResponseInterface 71 | { 72 | $action = RequestUtils::getPathSegment($request, 3); 73 | $result = $this->service->_list($action); 74 | return $this->responder->success($result); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudAdmin/Document/CsvDocument.php: -------------------------------------------------------------------------------- 1 | variables = $variables; 14 | } 15 | 16 | public function __toString(): string 17 | { 18 | $f = fopen('php://memory', 'r+'); 19 | fputcsv($f, $this->variables['columns']); 20 | foreach ($this->variables['records'] as $record) { 21 | fputcsv($f, $record); 22 | } 23 | rewind($f); 24 | return stream_get_contents($f); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudAdmin/Document/TemplateDocument.php: -------------------------------------------------------------------------------- 1 | masterTemplate = $masterTemplate; 18 | $this->contentTemplate = $contentTemplate; 19 | $this->variables = $variables; 20 | $this->template = new Template('html', $this->getFunctions()); 21 | $this->templatePath = ''; 22 | } 23 | 24 | private function getFunctions(): array 25 | { 26 | return array( 27 | 'lt' => function ($a, $b) { 28 | return $a < $b; 29 | }, 30 | 'gt' => function ($a, $b) { 31 | return $a > $b; 32 | }, 33 | 'le' => function ($a, $b) { 34 | return $a <= $b; 35 | }, 36 | 'ge' => function ($a, $b) { 37 | return $a >= $b; 38 | }, 39 | 'eq' => function ($a, $b) { 40 | return $a == $b; 41 | }, 42 | 'add' => function ($a, $b) { 43 | return $a + $b; 44 | }, 45 | 'sub' => function ($a, $b) { 46 | return $a - $b; 47 | }, 48 | 'prop' => function ($a, $b) { 49 | return $a[$b]; 50 | }, 51 | 'bool' => function ($a, $b, $c) { 52 | return $a ? $b : $c; 53 | }, 54 | 'or' => function ($a, $b) { 55 | return $a ?: $b; 56 | }, 57 | ); 58 | } 59 | 60 | public function addVariables(array $variables)/*: void*/ 61 | { 62 | $this->variables = array_merge($variables, $this->variables); 63 | } 64 | 65 | public function setTemplatePath(string $path)/*: void*/ 66 | { 67 | $this->templatePath = rtrim($path, '/'); 68 | } 69 | 70 | private function getHtmlFileContents(string $template): string 71 | { 72 | global $_HTML; 73 | if (isset($_HTML[$template])) { 74 | return $_HTML[$template]; 75 | } 76 | $filename = $this->templatePath . '/' . $template . '.html'; 77 | return file_get_contents($filename); 78 | } 79 | 80 | public function __toString(): string 81 | { 82 | $data = $this->variables; 83 | $content = $this->getHtmlFileContents($this->contentTemplate); 84 | $data['content'] = $this->template->render($content, $data); 85 | $master = $this->getHtmlFileContents($this->masterTemplate); 86 | return (string) $this->template->render($master, $data); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudAdmin/Template/Template.php: -------------------------------------------------------------------------------- 1 | escape = $escape; 13 | $this->functions = $functions; 14 | } 15 | 16 | private function escape(string $string): string 17 | { 18 | switch ($this->escape) { 19 | case 'html': 20 | return htmlspecialchars($string, ENT_QUOTES, 'UTF-8'); 21 | } 22 | return $string; 23 | } 24 | 25 | public function render(string $template, array $data): TemplateString 26 | { 27 | $tokens = $this->tokenize($template); 28 | $tree = $this->createSyntaxTree($tokens); 29 | $string = $this->renderChildren($tree, $data); 30 | return new TemplateString($string); 31 | } 32 | 33 | private function createNode(string $type, string $expression)/*: object*/ 34 | { 35 | return (object) array('type' => $type, 'expression' => $expression, 'children' => array(), 'value' => null); 36 | } 37 | 38 | private function tokenize(string $template): array 39 | { 40 | $parts = ['', $template]; 41 | $tokens = []; 42 | while (true) { 43 | $parts = explode('{{', $parts[1], 2); 44 | $tokens[] = $parts[0]; 45 | if (count($parts) != 2) { 46 | break; 47 | } 48 | $parts = $this->explode('}}', $parts[1], 2); 49 | $tokens[] = $parts[0]; 50 | if (count($parts) != 2) { 51 | break; 52 | } 53 | } 54 | return $tokens; 55 | } 56 | 57 | private function explode(string $separator, string $str, int $count = -1): array 58 | { 59 | $tokens = []; 60 | $token = ''; 61 | $quote = '"'; 62 | $escape = '\\'; 63 | $escaped = false; 64 | $quoted = false; 65 | for ($i = 0; $i < strlen($str); $i++) { 66 | $c = $str[$i]; 67 | if (!$quoted) { 68 | if ($c == $quote) { 69 | $quoted = true; 70 | } elseif (substr($str, $i, strlen($separator)) == $separator) { 71 | $tokens[] = $token; 72 | if (count($tokens) == $count - 1) { 73 | $token = substr($str, $i + strlen($separator)); 74 | break; 75 | } 76 | $token = ''; 77 | $i += strlen($separator) - 1; 78 | continue; 79 | } 80 | } else { 81 | if (!$escaped) { 82 | if ($c == $quote) { 83 | $quoted = false; 84 | } elseif ($c == $escape) { 85 | $escaped = true; 86 | } 87 | } else { 88 | $escaped = false; 89 | } 90 | } 91 | $token .= $c; 92 | } 93 | $tokens[] = $token; 94 | return $tokens; 95 | } 96 | 97 | private function createSyntaxTree(array $tokens)/*: object*/ 98 | { 99 | $root = $this->createNode('root', ''); 100 | $current = $root; 101 | $stack = array(); 102 | foreach ($tokens as $i => $token) { 103 | if ($i % 2 == 1) { 104 | if ($token == 'endif') { 105 | $type = 'endif'; 106 | $expression = false; 107 | } elseif ($token == 'endfor') { 108 | $type = 'endfor'; 109 | $expression = false; 110 | } elseif ($token == 'else') { 111 | $type = 'else'; 112 | $expression = false; 113 | } elseif (substr($token, 0, 7) == 'elseif:') { 114 | $type = 'elseif'; 115 | $expression = substr($token, 7); 116 | } elseif (substr($token, 0, 3) == 'if:') { 117 | $type = 'if'; 118 | $expression = substr($token, 3); 119 | } elseif (substr($token, 0, 4) == 'for:') { 120 | $type = 'for'; 121 | $expression = substr($token, 4); 122 | } else { 123 | $type = 'var'; 124 | $expression = $token; 125 | } 126 | if (in_array($type, array('endif', 'endfor', 'elseif', 'else'))) { 127 | if (count($stack)) { 128 | $current = array_pop($stack); 129 | } 130 | } 131 | if (in_array($type, array('if', 'for', 'var', 'elseif', 'else'))) { 132 | $node = $this->createNode($type, $expression); 133 | array_push($current->children, $node); 134 | } 135 | if (in_array($type, array('if', 'for', 'elseif', 'else'))) { 136 | array_push($stack, $current); 137 | $current = $node; 138 | } 139 | } else { 140 | array_push($current->children, $this->createNode('lit', $token)); 141 | } 142 | } 143 | return $root; 144 | } 145 | 146 | private function renderChildren(/*object*/$node, array $data): string 147 | { 148 | $result = ''; 149 | $ifNodes = array(); 150 | foreach ($node->children as $child) { 151 | switch ($child->type) { 152 | case 'if': 153 | $result .= $this->renderIfNode($child, $data); 154 | $ifNodes = array($child); 155 | break; 156 | case 'elseif': 157 | $result .= $this->renderElseIfNode($child, $ifNodes, $data); 158 | array_push($ifNodes, $child); 159 | break; 160 | case 'else': 161 | $result .= $this->renderElseNode($child, $ifNodes, $data); 162 | $ifNodes = array(); 163 | break; 164 | case 'for': 165 | $result .= $this->renderForNode($child, $data); 166 | $ifNodes = array(); 167 | break; 168 | case 'var': 169 | $result .= $this->renderVarNode($child, $data); 170 | $ifNodes = array(); 171 | break; 172 | case 'lit': 173 | $result .= $child->expression; 174 | $ifNodes = array(); 175 | break; 176 | } 177 | } 178 | return $result; 179 | } 180 | 181 | private function renderIfNode(/*object*/$node, array $data): string 182 | { 183 | $parts = $this->explode('|', $node->expression); 184 | $path = array_shift($parts); 185 | try { 186 | $value = $this->resolvePath($path, $data); 187 | $value = $this->applyFunctions($value, $parts, $data); 188 | } catch (\Throwable $e) { 189 | return $this->escape('{{if:' . $node->expression . '!!' . $e->getMessage() . '}}'); 190 | } 191 | $result = ''; 192 | if ($value) { 193 | $result .= $this->renderChildren($node, $data); 194 | } 195 | $node->value = $value; 196 | return $result; 197 | } 198 | 199 | private function renderElseIfNode(/*object*/$node, array $ifNodes, array $data): string 200 | { 201 | if (count($ifNodes) < 1 || $ifNodes[0]->type != 'if') { 202 | return $this->escape("{{elseif!!could not find matching `if`}}"); 203 | } 204 | $result = ''; 205 | $value = false; 206 | for ($i = 0; $i < count($ifNodes); $i++) { 207 | $value = $value || $ifNodes[$i]->value; 208 | } 209 | if (!$value) { 210 | $parts = $this->explode('|', $node->expression); 211 | $path = array_shift($parts); 212 | try { 213 | $value = $this->resolvePath($path, $data); 214 | $value = $this->applyFunctions($value, $parts, $data); 215 | } catch (\Throwable $e) { 216 | return $this->escape('{{elseif:' . $node->expression . '!!' . $e->getMessage() . '}}'); 217 | } 218 | if ($value) { 219 | $result .= $this->renderChildren($node, $data); 220 | } 221 | } 222 | $node->value = $value; 223 | return $result; 224 | } 225 | 226 | private function renderElseNode(/*object*/$node, array $ifNodes, array $data): string 227 | { 228 | if (count($ifNodes) < 1 || $ifNodes[0]->type != 'if') { 229 | return $this->escape("{{else!!could not find matching `if`}}"); 230 | } 231 | $result = ''; 232 | $value = false; 233 | for ($i = 0; $i < count($ifNodes); $i++) { 234 | $value = $value || $ifNodes[$i]->value; 235 | } 236 | if (!$value) { 237 | $result .= $this->renderChildren($node, $data); 238 | } 239 | return $result; 240 | } 241 | 242 | private function renderForNode(/*object*/$node, array $data): string 243 | { 244 | $parts = $this->explode('|', $node->expression); 245 | $pathParts = $this->explode(':', array_shift($parts), 3); 246 | if (count($pathParts) == 2) { 247 | list($var, $path) = $pathParts; 248 | $key = false; 249 | } elseif (count($pathParts) == 3) { 250 | list($var, $key, $path) = $pathParts; 251 | } else { 252 | return $this->escape('{{for:' . $node->expression . '!!' . "for must have `for:var:array` format" . '}}'); 253 | } 254 | try { 255 | $value = $this->resolvePath($path, $data); 256 | $value = $this->applyFunctions($value, $parts, $data); 257 | } catch (\Throwable $e) { 258 | return $this->escape('{{for:' . $node->expression . '!!' . $e->getMessage() . '}}'); 259 | } 260 | if (!is_array($value)) { 261 | return $this->escape('{{for:' . $node->expression . '!!' . "expression must evaluate to an array" . '}}'); 262 | } 263 | $result = ''; 264 | foreach ($value as $k => $v) { 265 | $data = array_merge($data, $key ? [$var => $v, $key => $k] : [$var => $v]); 266 | $result .= $this->renderChildren($node, $data); 267 | } 268 | return $result; 269 | } 270 | 271 | private function renderVarNode(/*object*/$node, array $data): string 272 | { 273 | $parts = $this->explode('|', $node->expression); 274 | $path = array_shift($parts); 275 | try { 276 | $value = $this->resolvePath($path, $data); 277 | $value = $this->applyFunctions($value, $parts, $data); 278 | } catch (\Throwable $e) { 279 | return $this->escape('{{' . $node->expression . '!!' . $e->getMessage() . '}}'); 280 | } 281 | if ($value instanceof TemplateString) { 282 | return $value; 283 | } 284 | return $this->escape((string) $value); 285 | } 286 | 287 | private function resolvePath(string $path, array $data)/*: object*/ 288 | { 289 | $current = $data; 290 | foreach ($this->explode('.', $path) as $p) { 291 | if (!array_key_exists($p, $current)) { 292 | throw new \Exception("path `$p` not found"); 293 | } 294 | $current = &$current[$p]; 295 | } 296 | return $current; 297 | } 298 | 299 | private function applyFunctions(/*object*/$value, array $parts, array $data)/*: object*/ 300 | { 301 | foreach ($parts as $part) { 302 | $function = $this->explode('(', rtrim($part, ')'), 2); 303 | $f = $function[0]; 304 | $arguments = isset($function[1]) ? $this->explode(',', $function[1]) : array(); 305 | $arguments = array_map(function ($argument) use ($data) { 306 | $argument = trim($argument); 307 | $len = strlen($argument); 308 | if ($argument[0] == '"' && $argument[$len - 1] == '"') { 309 | $argument = stripcslashes(substr($argument, 1, $len - 2)); 310 | } else if (!is_numeric($argument)) { 311 | $argument = $this->resolvePath($argument, $data); 312 | } 313 | return $argument; 314 | }, $arguments); 315 | array_unshift($arguments, $value); 316 | if (isset($this->functions[$f])) { 317 | $value = call_user_func_array($this->functions[$f], $arguments); 318 | } else { 319 | throw new \Exception("function `$f` not found"); 320 | } 321 | } 322 | return $value; 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudAdmin/Template/TemplateString.php: -------------------------------------------------------------------------------- 1 | string = $string; 12 | } 13 | 14 | public function __toString(): string 15 | { 16 | return $this->string; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/index.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'username' => 'php-crud-api', 15 | 'password' => 'php-crud-api', 16 | 'database' => 'php-crud-api', 17 | 'controllers' => 'columns', 18 | ], 19 | 'templatePath' => '../templates', 20 | ]); 21 | $request = RequestFactory::fromGlobals(); 22 | $ui = new Admin($config); 23 | $response = $ui->handle($request); 24 | ResponseUtils::output($response); 25 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | php -S localhost:8000 admin.php 3 | -------------------------------------------------------------------------------- /templates/column/create.html: -------------------------------------------------------------------------------- 1 |
5 | 6 |Created {{name}}: {{success}}
9 | 10 | -------------------------------------------------------------------------------- /templates/column/delete.html: -------------------------------------------------------------------------------- 1 | 5 | 6 |You are about to delete the column named "{{name}}"
9 | 10 |The action cannot be undone.
11 | 12 | -------------------------------------------------------------------------------- /templates/column/deleted.html: -------------------------------------------------------------------------------- 1 | 5 | 6 |Deleted {{name}}: {{success}}
9 | 10 | -------------------------------------------------------------------------------- /templates/column/list.html: -------------------------------------------------------------------------------- 1 | 5 | 6 |11 | | name | 12 |type | 13 |length | 14 |precision | 15 |scale | 16 |nullable | 17 |pk | 18 |fk | 19 |
---|---|---|---|---|---|---|---|---|
edit | 24 |{{column.name}} | 25 |{{column.type}} | 26 |{{column.length|or("-")}} | 27 |{{column.precision|or("-")}} | 28 |{{column.scale|or("-")}} | 29 |{{column.nullable|bool("yes","-")}} | 30 |{{column.pk|bool("yes","-")}} | 31 |32 | {{if:column.fk}} 33 | {{column.fk}} 34 | {{else}} 35 | - 36 | {{endif}} 37 | | 38 |
43 | Add column 44 | Delete table 45 |
-------------------------------------------------------------------------------- /templates/column/update.html: -------------------------------------------------------------------------------- 1 | 5 | 6 |Updated {{name}}: {{success}}
9 | 10 | -------------------------------------------------------------------------------- /templates/error/show.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |{{code}}
9 | 10 | message:{{message}}
12 | 13 | details:{{details}}-------------------------------------------------------------------------------- /templates/layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
Created {{name}}: {{success}}
8 | 9 | -------------------------------------------------------------------------------- /templates/table/delete.html: -------------------------------------------------------------------------------- 1 | 5 | 6 |The action cannot be undone.
9 | 10 | -------------------------------------------------------------------------------- /templates/table/deleted.html: -------------------------------------------------------------------------------- 1 | 5 | 6 |Deleted {{table}}: {{success}}
9 | 10 | -------------------------------------------------------------------------------- /templates/table/list.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |name | 10 |
---|
{{table}} | 15 |