├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── config ├── dependencies.php ├── middleware.php ├── routes.php ├── routes │ ├── admin.php │ ├── api.php │ ├── base.php │ ├── blog.php │ └── member.php └── settings │ ├── _base_.php │ ├── doctrine.php │ ├── logger.php │ ├── tracy.php │ └── view.php ├── console ├── migrations ├── Version20240829075026.php ├── Version20240829075240.php └── doctrine_migrations_class.php.tpl ├── phpinsights.php ├── public ├── .htaccess ├── css │ └── skeleton.css ├── favicon.ico └── index.php ├── rector.php ├── src ├── Console │ ├── CacheClearConsoleCommand.php │ └── CacheInitConsoleCommand.php ├── Controller │ ├── AdminController.php │ ├── Api │ │ └── PostController.php │ ├── AuthController.php │ ├── BlogController.php │ ├── HealthController.php │ └── HomeController.php ├── Entity │ ├── Post.php │ └── User.php ├── Middleware │ ├── BaseUrlMiddleware.php │ └── SessionMiddleware.php ├── Renderer │ ├── HtmlErrorRenderer.php │ ├── JsonErrorRenderer.php │ └── JsonRenderer.php ├── Repository │ ├── PostRepository.php │ └── UserRepository.php └── Services │ ├── Health.php │ └── Settings.php ├── tmpl ├── admin.twig ├── api.twig ├── blog.twig ├── error │ ├── 404.twig │ ├── default.twig │ └── layout.twig ├── index.twig ├── layout.twig └── login.twig └── var └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | \.ac-php-conf\.json 2 | vendor/ 3 | var/* 4 | composer.lock 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Changelog 2 | 3 | All notable changes to this project will be documented in this file. Dates are displayed in UTC. 4 | 5 | Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). 6 | 7 | #### [4.1.0](https://github.com/semhoun/slim-skeleton-mvc/compare/4.0.5...4.1.0) 8 | 9 | > 1 September 2024 10 | 11 | - Full use of DI, use doctrine 3 [`c664e26`](https://github.com/semhoun/slim-skeleton-mvc/commit/c664e26a4d11f2fc4655bae2ecb49b9840737a52) 12 | - Add API skeleton [`4c99bb7`](https://github.com/semhoun/slim-skeleton-mvc/commit/4c99bb7c09b8c57c3230d2ef68bb551f966b0ba3) 13 | - Migrate to Boostrap 5 [`cb79257`](https://github.com/semhoun/slim-skeleton-mvc/commit/cb79257a2b2eff49740f10da1743b4cd3d020229) 14 | 15 | #### [4.0.5](https://github.com/semhoun/slim-skeleton-mvc/compare/4.0.4...4.0.5) 16 | 17 | > 23 January 2023 18 | 19 | - Add Tracy to help dev [`b6932e6`](https://github.com/semhoun/slim-skeleton-mvc/commit/b6932e6d652e54c1f19988114d2796b602fb9408) 20 | - Add changelog [`763b64f`](https://github.com/semhoun/slim-skeleton-mvc/commit/763b64fa908366a2a79034bf29e178e1aeb4172c) 21 | - Use forked runtracy [`a8d37a9`](https://github.com/semhoun/slim-skeleton-mvc/commit/a8d37a9ca395bfc3c9cc55b6b3ddf09e978ad5d0) 22 | 23 | #### [4.0.4](https://github.com/semhoun/slim-skeleton-mvc/compare/4.0.3...4.0.4) 24 | 25 | > 17 August 2022 26 | 27 | - changed require to include and fixed typo [`#2`](https://github.com/semhoun/slim-skeleton-mvc/pull/2) 28 | - Fix issue #5 and upgrade to doctrine 2 [`b2574f1`](https://github.com/semhoun/slim-skeleton-mvc/commit/b2574f1705164742f2a020f46fc8a194d6a5a545) 29 | - Add exemple of custom css #4 [`220adba`](https://github.com/semhoun/slim-skeleton-mvc/commit/220adba5474222d448508744154ac671ecc9c959) 30 | - Update codacity badge [`e1f5e26`](https://github.com/semhoun/slim-skeleton-mvc/commit/e1f5e26095aa92adbc6f225fdcca40098ad4d96a) 31 | 32 | #### [4.0.3](https://github.com/semhoun/slim-skeleton-mvc/compare/4.0.2...4.0.3) 33 | 34 | > 6 January 2021 35 | 36 | - Replace tab by space [`3ae7425`](https://github.com/semhoun/slim-skeleton-mvc/commit/3ae74258a76ff1db165295fb783818e1bd5da2de) 37 | - Add proxy detection to make it work behind proxy like traefik [`cdf8257`](https://github.com/semhoun/slim-skeleton-mvc/commit/cdf82571aca75bf728feb7db1bb7b91a85eace38) 38 | 39 | #### [4.0.2](https://github.com/semhoun/slim-skeleton-mvc/compare/4.0.1...4.0.2) 40 | 41 | > 19 January 2020 42 | 43 | - Replace space by tabs [`d7c8686`](https://github.com/semhoun/slim-skeleton-mvc/commit/d7c868674dd6d590fa0238e0ca4ca3b7d267722f) 44 | - Update to stalbe version [`bdf74b5`](https://github.com/semhoun/slim-skeleton-mvc/commit/bdf74b565cb1aafd9245c448093d1e4dd77ee858) 45 | - Add base_path [`f382bc3`](https://github.com/semhoun/slim-skeleton-mvc/commit/f382bc3dc82e5396100261192ff6d077b9b62683) 46 | 47 | #### [4.0.1](https://github.com/semhoun/slim-skeleton-mvc/compare/4.0.0...4.0.1) 48 | 49 | > 5 October 2019 50 | 51 | - Update twig-view [`97514b6`](https://github.com/semhoun/slim-skeleton-mvc/commit/97514b6a0d2a041b8c83e27b64f244b2eb2708c0) 52 | - Remove factories no more needed [`e99dcc5`](https://github.com/semhoun/slim-skeleton-mvc/commit/e99dcc57f300839b3a4108b0e2436f74701673f3) 53 | - Fix Readme [`27c6258`](https://github.com/semhoun/slim-skeleton-mvc/commit/27c62581353c42cb01d193235ee00a8eb56afff5) 54 | 55 | ### [4.0.0](https://github.com/semhoun/slim-skeleton-mvc/compare/3.0.0...4.0.0) 56 | 57 | > 15 September 2019 58 | 59 | - Add a Codacy badge to README.md [`#1`](https://github.com/semhoun/slim-skeleton-mvc/pull/1) 60 | - Update to Slim4 [`1be11d4`](https://github.com/semhoun/slim-skeleton-mvc/commit/1be11d4e6e927eb8d1f8290dc5edae52f8a02cf9) 61 | - Use bootstrap template [`c8f86ee`](https://github.com/semhoun/slim-skeleton-mvc/commit/c8f86ee502345b8ec3d7c79422ed63442696da23) 62 | - Update Readme [`02d5c39`](https://github.com/semhoun/slim-skeleton-mvc/commit/02d5c39a1a4d7691082256f60458835bf4e3002a) 63 | 64 | #### 3.0.0 65 | 66 | > 21 August 2019 67 | 68 | - Adding JWT auth [`38a9563`](https://github.com/semhoun/slim-skeleton-mvc/commit/38a95635b72fa6f88bcdbef786fc884776ff8a68) 69 | - Directory refactoring [`852fa4d`](https://github.com/semhoun/slim-skeleton-mvc/commit/852fa4db90a97ec1c018fcabf6f411385a375295) 70 | - First init [`d29d33d`](https://github.com/semhoun/slim-skeleton-mvc/commit/d29d33d39c82822fe03711f9b7a462f72a406545) 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 vhchung 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 | # Slim 4 MVC Skeleton 2 | 3 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/62644bc058af464eb2cfcf564c3500d6)](https://www.codacy.com/gh/semhoun/slim-skeleton-mvc/dashboard?utm_source=github.com&utm_medium=referral&utm_content=semhoun/slim-skeleton-mvc&utm_campaign=Badge_Grade) [![Latest Stable Version](https://poser.pugx.org/semhoun/slim-skeleton-mvc/v/stable)](https://packagist.org/packages/semhoun/slim-skeleton-mvc) [![Total Downloads](https://poser.pugx.org/semhoun/slim-skeleton-mvc/downloads)](https://packagist.org/packages/semhoun/slim-skeleton-mvc) [![Latest Unstable Version](https://poser.pugx.org/semhoun/slim-skeleton-mvc/v/unstable)](https://packagist.org/packages/semhoun/slim-skeleton-mvc) [![License](https://poser.pugx.org/semhoun/slim-skeleton-mvc/license)](https://packagist.org/packages/semhoun/slim-skeleton-mvc) [![Monthly Downloads](https://poser.pugx.org/semhoun/slim-skeleton-mvc/d/monthly)](https://packagist.org/packages/semhoun/slim-skeleton-mvc) 4 | 5 | This is a simple web application skeleton project that uses the [Slim4 Framework](http://www.slimframework.com/): 6 | * [PHP-DI](http://php-di.org/) as dependency injection container 7 | * [Slim-Psr7](https://github.com/slimphp/Slim-Psr7) as PSR-7 implementation 8 | * [Doctrine](https://github.com/doctrine/orm) as ORM 9 | * [Twig](https://twig.symfony.com/) as template engine 10 | * [Monolog](https://github.com/Seldaek/monolog) 11 | * [Symfony Console](https://github.com/symfony/console) 12 | * [Proxy Detection](https://github.com/akrabat/proxy-detection-middleware) 13 | * [PHP Insights](https://phpinsights.com/) and [Rector](https://getrector.com/) for code quality 14 | 15 | 16 | ## Prepare 17 | 18 | 1. Create your project: 19 | ```bash 20 | composer create-project semhoun/slim-skeleton-mvc [your-app] 21 | ``` 22 | 2. Create database (inside your-app): `./console.php migrations:migrate` 23 | 24 | 25 | ## Run it: 26 | 27 | 1. `cd [your-app]` 28 | 2. `php -S 0.0.0.0:8888 -t public/` 29 | 3. Browse to http://localhost:8888 30 | 31 | 32 | ## Notice 33 | 34 | - Set `var` folder permission to writable when deploy to production environment 35 | - Default login/password is *admin*/*admin* -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "semhoun/slim-skeleton-mvc", 3 | "description": "Simple Slim Framework 4 skeleton with Twig, Monolog, Doctrine in Sqlite.", 4 | "license": "MIT", 5 | "type": "project", 6 | "keywords": [ 7 | "slim-framework", 8 | "skeleton", 9 | "mvc", 10 | "sqlite", 11 | "twig", 12 | "monolog", 13 | "doctrine", 14 | "console", 15 | "api" 16 | ], 17 | "authors": [ 18 | { 19 | "name": "Nathanael Semhoun", 20 | "email": "nathanael@semhoun.net", 21 | "homepage": "http://www.semhoun.net" 22 | } 23 | ], 24 | "autoload": { 25 | "psr-4": { 26 | "App\\": "src/" 27 | } 28 | }, 29 | "require": { 30 | "php": "^8.0", 31 | "ext-sqlite3": "*", 32 | "ext-json": "*", 33 | "slim/slim": "^4.14", 34 | "slim/psr7": "^1.7", 35 | "doctrine/orm": "^3", 36 | "doctrine/dbal": "^4", 37 | "doctrine/migrations": "^3", 38 | "symfony/cache": "^7", 39 | "php-di/slim-bridge": "^3.4", 40 | "monolog/monolog": "^3.6", 41 | "symfony/console": "^6.0", 42 | "slim/twig-view": "^3.4", 43 | "akrabat/proxy-detection-middleware": "^1.0" 44 | }, 45 | "require-dev": { 46 | "nunomaduro/phpinsights": "^2.11", 47 | "rector/rector": "^1.1", 48 | "semhoun/slim-tracy": "^1.0" 49 | }, 50 | "scripts": { 51 | "start": "php -S localhost:8080 -t public public/index.php", 52 | "rector-check": "rector process --dry-run", 53 | "rector-fix": "rector process", 54 | "insights-check": "phpinsights --config-path=./phpinsights.php", 55 | "insights-fix": "phpinsights --config-path=./phpinsights.php -- fix" 56 | }, 57 | "config": { 58 | "allow-plugins": { 59 | "dealerdirect/phpcodesniffer-composer-installer": true 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /config/dependencies.php: -------------------------------------------------------------------------------- 1 | static fn (Settings $settings, Doctrine\ORM\Configuration $conf): Doctrine\DBAL\Connection => \Doctrine\DBAL\DriverManager::getConnection($settings->get('doctrine.connection'), $conf), 18 | // Doctrine Config used by entity manager and Tracy 19 | \Doctrine\ORM\Configuration::class => static function (Settings $settings): Doctrine\ORM\Configuration { 20 | if ($settings->get('debug')) { 21 | $queryCache = new ArrayAdapter(); 22 | $metadataCache = new ArrayAdapter(); 23 | } else { 24 | $queryCache = new PhpFilesAdapter('queries', 0, $settings->get('cache_dir')); 25 | $metadataCache = new PhpFilesAdapter('metadata', 0, $settings->get('cache_dir')); 26 | } 27 | 28 | $config = new \Doctrine\ORM\Configuration(); 29 | $config->setMetadataCache($metadataCache); 30 | $driverImpl = new \Doctrine\ORM\Mapping\Driver\AttributeDriver($settings->get('doctrine.entity_path'), true); 31 | $config->setMetadataDriverImpl($driverImpl); 32 | $config->setQueryCache($queryCache); 33 | $config->setProxyDir($settings->get('cache_dir') . '/proxy'); 34 | $config->setProxyNamespace('App\Proxies'); 35 | 36 | if ($settings->get('debug')) { 37 | $config->setAutoGenerateProxyClasses(true); 38 | } else { 39 | $config->setAutoGenerateProxyClasses(false); 40 | } 41 | 42 | return $config; 43 | }, 44 | // Doctrine EntityManager. 45 | EntityManager::class => static fn (\Doctrine\ORM\Configuration $config, \Doctrine\DBAL\Connection $connection): EntityManager => new EntityManager($connection, $config), 46 | EntityManagerInterface::class => DI\get(EntityManager::class), 47 | // Settings. 48 | Settings::class => DI\factory([Settings::class, 'load']), 49 | Logger::class => static function (Settings $settings): Logger { 50 | $logger = new Logger($settings->get('logger.name')); 51 | $processor = new UidProcessor(); 52 | $logger->pushProcessor($processor); 53 | 54 | $handler = new StreamHandler($settings->get('logger.path'), $settings->get('logger.level')); 55 | $logger->pushHandler($handler); 56 | 57 | return $logger; 58 | }, 59 | Twig::class => static function (Settings $settings, \Twig\Profiler\Profile $profile): Twig { 60 | $view = Twig::create($settings->get('view.template_path'), $settings->get('view.twig')); 61 | if ($settings->get('debug')) { 62 | // Add extensions 63 | $view->addExtension(new \Twig\Extension\ProfilerExtension($profile)); 64 | $view->addExtension(new \Twig\Extension\DebugExtension()); 65 | } 66 | return $view; 67 | }, 68 | ]; 69 | -------------------------------------------------------------------------------- /config/middleware.php: -------------------------------------------------------------------------------- 1 | getContainer(); 15 | $settings = $container->get(Settings::class); 16 | 17 | $app->add(TwigMiddleware::create($app, $container->get(Twig::class))); 18 | 19 | $app->add($container->get(SessionMiddleware::class)); 20 | $app->add($container->get(BaseUrlMiddleware::class)); 21 | 22 | $app->add(new \RKA\Middleware\ProxyDetection()); 23 | 24 | // Add error handling middleware. 25 | if ($settings->get('debug')) { 26 | $app->add(new SlimTracy\Middlewares\TracyMiddleware($app, $settings->get('tracy'))); 27 | Debugger::enable(Debugger::Development); 28 | } 29 | $errorMiddleware = $app->addErrorMiddleware($settings->get('debug'), true, true); 30 | $errorHandler = $errorMiddleware->getDefaultErrorHandler(); 31 | $errorHandler->registerErrorRenderer('text/html', \App\Renderer\HtmlErrorRenderer::class); 32 | $errorHandler->registerErrorRenderer('application/json', \App\Renderer\JsonErrorRenderer::class); 33 | $errorHandler->setDefaultErrorRenderer('application/json', \App\Renderer\JsonErrorRenderer::class); 34 | }; 35 | -------------------------------------------------------------------------------- /config/routes.php: -------------------------------------------------------------------------------- 1 | getContainer(); 12 | $settings = $container->get(Settings::class); 13 | 14 | $app->get('/health', [Controller\HealthController::class, 'health'])->setName('health'); 15 | $settings = $app->getContainer()->get(Settings::class); 16 | if ($settings->get('debug') && $settings->get('tracy.configs.ConsoleEnable')) { 17 | $app->post('/console', 'SlimTracy\Controllers\SlimTracyConsole:index'); 18 | } 19 | 20 | // Activating all routes 21 | foreach (glob(Settings::getAppRoot() . '/config/routes/*.php') as $file) { 22 | $route = require $file; 23 | $route($app); 24 | } 25 | 26 | // Not Found 27 | $app->map( 28 | ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], 29 | '/{routes:.*}', 30 | static function (Request $request): void { 31 | throw new Slim\Exception\HttpNotFoundException($request); 32 | } 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /config/routes/admin.php: -------------------------------------------------------------------------------- 1 | group('/admin', static function (Group $group): void { 12 | $group->get('', [\App\Controller\AdminController::class, 'view'])->setName('admin'); 13 | $group->get('/database', [\App\Controller\AdminController::class, 'databasesInfo'])->setName('adminDatabasesInfo'); 14 | $group->get('/database/{id}', [\App\Controller\AdminController::class, 'databaseExport'])->setName('adminDatabaseExport'); 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /config/routes/api.php: -------------------------------------------------------------------------------- 1 | group('/api', static function (Group $group): void { 12 | $group->get('/post', [\App\Controller\Api\PostController::class, 'getAll'])->setName('apiPostGetAll'); 13 | $group->get('/post/{id}', [\App\Controller\Api\PostController::class, 'get']); 14 | $group->post('/post', [\App\Controller\Api\PostController::class, 'add']); 15 | $group->delete('/post/{id}', [\App\Controller\Api\PostController::class, 'delete']); 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /config/routes/base.php: -------------------------------------------------------------------------------- 1 | get('/', [\App\Controller\HomeController::class, 'index'])->setName('home'); 11 | $app->get('/api_info', [\App\Controller\HomeController::class, 'apiInfo'])->setName('apiInfo'); 12 | $app->get('/error', [\App\Controller\HomeController::class, 'error'])->setName('error'); 13 | }; 14 | -------------------------------------------------------------------------------- /config/routes/blog.php: -------------------------------------------------------------------------------- 1 | get('/blog/{id}', [\App\Controller\BlogController::class, 'view'])->setName('blog'); 11 | }; 12 | -------------------------------------------------------------------------------- /config/routes/member.php: -------------------------------------------------------------------------------- 1 | group('/member', static function (Group $group): void { 12 | $group->map(['GET', 'POST'], '/login', [\App\Controller\AuthController::class, 'login'])->setName('login'); 13 | $group->get('/logout', [\App\Controller\AuthController::class, 'logout'])->setName('logout'); 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /config/settings/_base_.php: -------------------------------------------------------------------------------- 1 | phpversion(), 12 | // Is debug moderm 13 | 'debug' => $debug, 14 | // API Base path 15 | 'api_base_path' => '/api', 16 | // 'Temprorary directory 17 | 'temporary_path' => Settings::getAppRoot() . '/var/tmp', 18 | // Route cache 19 | 'cache_dir' => Settings::getAppRoot() . '/var/cache', 20 | ]; 21 | -------------------------------------------------------------------------------- /config/settings/doctrine.php: -------------------------------------------------------------------------------- 1 | [Settings::getAppRoot() . '/src/Entity'], 9 | 'connection' => [ 10 | 'driver' => 'pdo_sqlite', 11 | 'path' => Settings::getAppRoot() . '/var/database.sqlite', 12 | ], 13 | 'migrations' => [ 14 | 'table_storage' => [ 15 | 'table_name' => 'db_version', 16 | 'version_column_name' => 'version', 17 | 'version_column_length' => 1024, 18 | 'executed_at_column_name' => 'executed_at', 19 | 'execution_time_column_name' => 'execution_time', 20 | ], 21 | 'migrations_paths' => [ 22 | 'Mvno' => Settings::getAppRoot() . '/migrations', 23 | ], 24 | 'all_or_nothing' => true, 25 | 'transactional' => true, 26 | 'check_database_platform' => true, 27 | 'organize_migrations' => 'none', 28 | 'connection' => null, 29 | 'em' => null, 30 | 'custom_template' => Settings::getAppRoot() 31 | . '/migrations/doctrine_migrations_class.php.tpl', 32 | ], 33 | ]; 34 | -------------------------------------------------------------------------------- /config/settings/logger.php: -------------------------------------------------------------------------------- 1 | 'app', 13 | 'path' => $docker ? 14 | 'php://stdout' 15 | : Settings::getAppRoot() . '/var/log/app.log', 16 | 'level' => $debug ? Level::Debug : Level::Info, 17 | ]; 18 | -------------------------------------------------------------------------------- /config/settings/tracy.php: -------------------------------------------------------------------------------- 1 | 0, 19 | 'showSlimRouterPanel' => 0, 20 | 'showSlimEnvironmentPanel' => 0, 21 | 'showSlimRequestPanel' => 1, 22 | 'showSlimResponsePanel' => 1, 23 | 'showSlimContainer' => 0, 24 | 'showEloquentORMPanel' => 0, 25 | 'showTwigPanel' => 1, 26 | 'showDoctrinePanel' => 1, 27 | 'showProfilerPanel' => 0, 28 | 'showVendorVersionsPanel' => 0, 29 | 'showXDebugHelper' => 0, 30 | 'showIncludedFiles' => 0, 31 | 'showConsolePanel' => 0, 32 | 'configs' => [ 33 | 'XDebugHelperIDEKey' => 'PHPSTORM', 34 | 'ConsoleEnable' => 0, 35 | 'ConsoleNoLogin' => 0, 36 | 'ConsoleAccounts' => [ 37 | 'dev' => '34c6fceca75e456f25e7e99531e2425c6c1de443', // = sha1('dev') 38 | ], 39 | 'ConsoleHashAlgorithm' => 'sha1', 40 | 'ConsoleHomeDirectory' => Settings::getAppRoot(), 41 | // terminal.js full URI 42 | 'ConsoleTerminalJs' => $consoleTerminalJs, 43 | // terminal.css full URI 44 | 'ConsoleTerminalCss' => $consoleTerminalCss, 45 | 'ConsoleFromEncoding' => 'UTF-8', 46 | 'ProfilerPanel' => [ 47 | 'show' => [ 48 | 'memoryUsageChart' => 1, 49 | 'shortProfiles' => true, 50 | 'timeLines' => true, 51 | ], 52 | ], 53 | 'Container' => [ 54 | // Container entry name 55 | 'Doctrine' => \Doctrine\ORM\Configuration::class, // must be a configuration DBAL or ORM 56 | 'Twig' => \Twig\Profiler\Profile::class, 57 | ], 58 | ], 59 | ]; 60 | -------------------------------------------------------------------------------- /config/settings/view.php: -------------------------------------------------------------------------------- 1 | Settings::getAppRoot() . '/tmpl', 11 | 'twig' => [ 12 | 'cache' => Settings::getAppRoot() . '/var/cache/twig', 13 | 'debug' => true, 14 | 'auto_reload' => $debug, 15 | ], 16 | 'base_path' => '/app', 17 | ]; 18 | -------------------------------------------------------------------------------- /console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | get('debug')) { 25 | // Compile and cache container. 26 | $containerBuilder->enableCompilation($settings->get('cache_dir') . '/container'); 27 | } 28 | 29 | // Set up dependencies 30 | $containerBuilder->addDefinitions($rootPath . '/config/dependencies.php'); 31 | 32 | // Build PHP-DI Container instance 33 | $container = $containerBuilder->build(); 34 | 35 | // Doctrine migration 36 | $dependencyFactory = DependencyFactory::fromEntityManager( 37 | new ConfigurationArray($container->get(Settings::class)->get('doctrine.migrations')), 38 | new ExistingEntityManager($container->get(EntityManager::class)) 39 | ); 40 | 41 | // Build the app 42 | $cli = new Application(); 43 | $cli->add($container->get(\App\Console\CacheClearConsoleCommand::class)); 44 | $cli->add($container->get(\App\Console\CacheInitConsoleCommand::class)); 45 | 46 | $cli->addCommands([ 47 | new Command\DumpSchemaCommand($dependencyFactory), 48 | new Command\ExecuteCommand($dependencyFactory), 49 | new Command\GenerateCommand($dependencyFactory), 50 | new Command\LatestCommand($dependencyFactory), 51 | new Command\ListCommand($dependencyFactory), 52 | new Command\MigrateCommand($dependencyFactory), 53 | new Command\RollupCommand($dependencyFactory), 54 | new Command\StatusCommand($dependencyFactory), 55 | new Command\SyncMetadataCommand($dependencyFactory), 56 | new Command\VersionCommand($dependencyFactory), 57 | ]); 58 | 59 | try { 60 | exit($cli->run()); 61 | } catch (Throwable $exception) { 62 | echo $exception->getMessage(); 63 | exit(1); 64 | } 65 | -------------------------------------------------------------------------------- /migrations/Version20240829075026.php: -------------------------------------------------------------------------------- 1 | addSql('CREATE TABLE user (id INTEGER PRIMARY KEY AUTOINCREMENT NOT null, username CHAR(30) NOT null, password CHAR(60) not null, first_name CHAR(50), last_name CHAR(50), email CHAR(50));'); 20 | $this->addSql("INSERT INTO user VALUES(1,'admin','" . password_hash('admin', PASSWORD_DEFAULT) . "', 'Administator', 'THE', 'admin@admin.com');"); 21 | } 22 | 23 | public function down(Schema $schema): void 24 | { 25 | $this->addSql('DROP TABLE user'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /migrations/Version20240829075240.php: -------------------------------------------------------------------------------- 1 | addSql('CREATE TABLE post (id INTEGER PRIMARY KEY AUTOINCREMENT NOT null, title CHAR(100) default null, content TEXT NOT null);'); 20 | $this->addSQL("INSERT INTO post VALUES(1,'First blog post','This is sample blog post. If you see this content, doctrine is working fine.');"); 21 | $this->addSQL("INSERT INTO post VALUES(2,'Second blog post','This is the second blog post.');"); 22 | } 23 | 24 | public function down(Schema $schema): void 25 | { 26 | $this->addSql('DROP TABLE post'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /migrations/doctrine_migrations_class.php.tpl: -------------------------------------------------------------------------------- 1 | ; 4 | 5 | use Doctrine\DBAL\Schema\Schema; 6 | use Doctrine\Migrations\AbstractMigration; 7 | 8 | final class extends AbstractMigration 9 | { 10 | public function getDescription(): string 11 | { 12 | return ''; 13 | } 14 | 15 | public function up(Schema $schema): void 16 | { 17 | 18 | } 19 | 20 | public function down(Schema $schema): void 21 | { 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /phpinsights.php: -------------------------------------------------------------------------------- 1 | 'default', 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | IDE 25 | |-------------------------------------------------------------------------- 26 | | 27 | | This options allow to add hyperlinks in your terminal to quickly open 28 | | files in your favorite IDE while browsing your PhpInsights report. 29 | | 30 | | Supported: "textmate", "macvim", "emacs", "sublime", "phpstorm", 31 | | "atom", "vscode". 32 | | 33 | | If you have another IDE that is not in this list but which provide an 34 | | url-handler, you could fill this config with a pattern like this: 35 | | 36 | | myide://open?url=file://%f&line=%l 37 | | 38 | */ 39 | 40 | 'ide' => null, 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Configuration 45 | |-------------------------------------------------------------------------- 46 | | 47 | | Here you may adjust all the various `Insights` that will be used by PHP 48 | | Insights. You can either add, remove or configure `Insights`. Keep in 49 | | mind, that all added `Insights` must belong to a specific `Metric`. 50 | | 51 | */ 52 | 53 | 'exclude' => [ 54 | 'var', 55 | ], 56 | 57 | 'add' => [ 58 | // ExampleMetric::class => [ 59 | // ExampleInsight::class, 60 | // ] 61 | ], 62 | 63 | 'remove' => [ 64 | SlevomatCodingStandard\Sniffs\ControlStructures\DisallowEmptySniff::class, 65 | SlevomatCodingStandard\Sniffs\Functions\UnusedParameterSniff::class, 66 | ], 67 | 68 | 'config' => [ 69 | // ExampleInsight::class => [ 70 | // 'key' => 'value', 71 | // ], 72 | ], 73 | 74 | /* 75 | |-------------------------------------------------------------------------- 76 | | Requirements 77 | |-------------------------------------------------------------------------- 78 | | 79 | | Here you may define a level you want to reach per `Insights` category. 80 | | When a score is lower than the minimum level defined, then an error 81 | | code will be returned. This is optional and individually defined. 82 | | 83 | */ 84 | 85 | 'requirements' => [ 86 | // 'min-quality' => 0, 87 | // 'min-complexity' => 0, 88 | // 'min-architecture' => 0, 89 | // 'min-style' => 0, 90 | // 'disable-security-check' => false, 91 | ], 92 | 93 | /* 94 | |-------------------------------------------------------------------------- 95 | | Threads 96 | |-------------------------------------------------------------------------- 97 | | 98 | | Here you may adjust how many threads (core) PHPInsights can use to perform 99 | | the analysis. This is optional, don't provide it and the tool will guess 100 | | the max core number available. It accepts null value or integer > 0. 101 | | 102 | */ 103 | 104 | 'threads' => null, 105 | 106 | /* 107 | |-------------------------------------------------------------------------- 108 | | Timeout 109 | |-------------------------------------------------------------------------- 110 | | Here you may adjust the timeout (in seconds) for PHPInsights to run before 111 | | a ProcessTimedOutException is thrown. 112 | | This accepts an int > 0. Default is 60 seconds, which is the default value 113 | | of Symfony's setTimeout function. 114 | | 115 | */ 116 | 117 | 'timeout' => 60, 118 | ]; 119 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | SetEnv APPLICATION_ENV development 2 | 3 | 4 | RewriteEngine On 5 | 6 | RewriteCond %{REQUEST_FILENAME} !-f 7 | RewriteCond %{REQUEST_FILENAME} !-d 8 | RewriteRule ^ index.php [QSA,L] 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/css/skeleton.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: blue; 3 | } 4 | 5 | body { 6 | padding-bottom: 10px; 7 | } 8 | 9 | .navbar { 10 | margin-bottom: 10px; 11 | } 12 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semhoun/slim-skeleton-mvc/f3af7544786422fbda06b85f4988d96f27743e7f/public/favicon.ico -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | get('debug')) { 21 | // Compile and cache container. 22 | $containerBuilder->enableCompilation($settings->get('cache_dir').'/container'); 23 | } 24 | 25 | // Set up dependencies 26 | $containerBuilder->addDefinitions($rootPath.'/config/dependencies.php'); 27 | 28 | // Build PHP-DI Container instance 29 | $container = $containerBuilder->build(); 30 | 31 | // Instantiate the app 32 | $app = \DI\Bridge\Slim\Bridge::create($container); 33 | 34 | // Register middleware 35 | $middleware = require $rootPath . '/config/middleware.php'; 36 | $middleware($app); 37 | 38 | // Register routes 39 | $routes = require $rootPath . '/config/routes.php'; 40 | $routes($app); 41 | 42 | // Set the cache file for the routes. Note that you have to delete this file 43 | // whenever you change the routes. 44 | if (! $settings->get('debug')) { 45 | $app->getRouteCollector()->setCacheFile($settings->get('cache_dir').'/route'); 46 | } 47 | 48 | // Add the routing middleware. 49 | $app->addRoutingMiddleware(); 50 | 51 | // Add Body Parsing Middleware 52 | $app->addBodyParsingMiddleware(); 53 | 54 | // Run the app 55 | $app->run(); 56 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([ 10 | __DIR__ . '/src', 11 | __DIR__ . '/config', 12 | ]) 13 | ->withPhpSets(php82: true) 14 | ->withRules([ 15 | TypedPropertyFromStrictConstructorRector::class, 16 | ]) 17 | ->withAttributesSets(doctrine: true); 18 | -------------------------------------------------------------------------------- /src/Console/CacheClearConsoleCommand.php: -------------------------------------------------------------------------------- 1 | settings->get('cache_dir')]; 25 | foreach ($cacheDirs as $cacheDir) { 26 | if (! file_exists($cacheDir)) { 27 | continue; 28 | } 29 | 30 | $this->removeDirectoryContent($cacheDir); 31 | } 32 | 33 | return Command::SUCCESS; 34 | } 35 | 36 | private function removeDirectoryContent(string $path): void 37 | { 38 | $files = glob($path . '/*'); 39 | if ($files === false) { 40 | return; 41 | } 42 | 43 | foreach ($files as $file) { 44 | is_dir($file) ? $this->removeDirectory($file) : unlink($file); 45 | } 46 | } 47 | 48 | private function removeDirectory(string $path): void 49 | { 50 | $files = glob($path . '/*'); 51 | if ($files === false) { 52 | return; 53 | } 54 | 55 | foreach ($files as $file) { 56 | is_dir($file) ? $this->removeDirectory($file) : unlink($file); 57 | } 58 | rmdir($path); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Console/CacheInitConsoleCommand.php: -------------------------------------------------------------------------------- 1 | entityManager->getProxyFactory(); 28 | $proxyFactory->generateProxyClasses( 29 | $this->entityManager->getMetadataFactory()->getAllMetadata() 30 | ); 31 | 32 | return Command::SUCCESS; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Controller/AdminController.php: -------------------------------------------------------------------------------- 1 | view->render($response, 'admin.twig'); 25 | } 26 | 27 | public function databasesInfo(Request $request, Response $response): Response 28 | { 29 | return $this->renderer->json($response, [ 30 | [ 'id' => 'Post', 'title' => 'Blog Post'], 31 | [ 'id' => 'User', 'title' => 'Users'], 32 | ]); 33 | } 34 | 35 | public function databaseExport(Request $request, Response $response, string $id): Response 36 | { 37 | $data = $this->entityManager->getRepository('\\App\\Entity\\' . $id)->adminExport(); 38 | return $this->renderer->json($response, $data); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Controller/Api/PostController.php: -------------------------------------------------------------------------------- 1 | entityManager->getRepository(\App\Entity\Post::class)->find($postId); 27 | } catch (\Exception $e) { 28 | throw new \Slim\Exception\HttpInternalServerErrorException($request, $e->getMessage()); 29 | } 30 | 31 | // Using interface JsonSerializable to make simplify 32 | return $this->renderer->json($response, $post); 33 | } 34 | 35 | public function getAll(Request $request, Response $response): Response 36 | { 37 | try { 38 | $posts = $this->entityManager->getRepository(\App\Entity\Post::class)->findAll(); 39 | } catch (\Exception $e) { 40 | throw new \Slim\Exception\HttpInternalServerErrorException($request, $e->getMessage()); 41 | } 42 | 43 | // Using html output for debug 44 | if (preg_match('/html/', $request->getHeaderLine('Accept'))) { 45 | return $this->renderer->html($response, $posts); 46 | } 47 | return $this->renderer->json($response, $posts); 48 | } 49 | 50 | public function add(Request $request, Response $response): Response 51 | { 52 | $data = $request->getParsedBody(); 53 | 54 | if (empty($data['slug']) || empty($data['content'])) { 55 | throw new \Slim\Exception\HttpBadRequestException($request, 'Mandatory param not setted'); 56 | } 57 | 58 | $post = new \App\Entity\Post(); 59 | if (! empty($data['title'])) { 60 | $post->setTitle($data['title']); 61 | } 62 | $post->setContent($data['content']); 63 | 64 | try { 65 | $this->entityManager->persist($post); 66 | $this->entityManager->flush(); 67 | } catch (\Exception $e) { 68 | throw new \Slim\Exception\HttpInternalServerErrorException($request, $e->getMessage()); 69 | } 70 | // Using interface JsonSerializable to make simplify 71 | return $this->renderer->json($response); 72 | } 73 | 74 | public function delete(Request $request, Response $response, int $postId): Response 75 | { 76 | try { 77 | $post = $this->entityManager->getRepository(\App\Entity\Post::class)->find($postId); 78 | $this->entityManager->remove($post); 79 | $this->entityManager->flush(); 80 | } catch (\Exception $e) { 81 | throw new \Slim\Exception\HttpInternalServerErrorException($request, $e->getMessage()); 82 | } 83 | 84 | // Using interface JsonSerializable to make simplify 85 | return $this->renderer->json($response); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Controller/AuthController.php: -------------------------------------------------------------------------------- 1 | getMethod() === 'POST') { 25 | $data = $request->getParsedBody(); 26 | 27 | if (empty($data['uname']) || empty($data['pswd'])) { 28 | return $this->view->render($response, 'login.twig', ['flash' => ['Empty value in login/password'], 'uinfo' => $request->getAttribute('uinfo')]); 29 | } 30 | 31 | // Check the user username / pass 32 | $uinfo = $this->auth($data['uname'], $data['pswd']); 33 | if ($uinfo === null) { 34 | return $this->view->render($response, 'login.twig', ['flash' => ['Invalid login/password'], 'uinfo' => $request->getAttribute('uinfo')]); 35 | } 36 | 37 | $session = $request->getAttribute('session'); 38 | $session['logged'] = true; 39 | $session['uinfo'] = [ 40 | 'id' => $uinfo->getId(), 41 | 'firstname' => $uinfo->getFirstName(), 42 | 'lastname' => $uinfo->getLastName(), 43 | 'email' => $uinfo->getEmail(), 44 | ]; 45 | 46 | return $response->withStatus(302)->withHeader('Location', '/'); 47 | } 48 | return $this->view->render($response, 'login.twig', ['uinfo' => $request->getAttribute('uinfo')]); 49 | } 50 | 51 | public function logout(Request $request, Response $response): Response 52 | { 53 | $session = $request->getAttribute('session'); 54 | $session['logged'] = false; 55 | unset($session['uinfo']); 56 | return $response->withStatus(302)->withHeader('Location', '/'); 57 | } 58 | 59 | private function auth(string $uname, string $pswd): ?\App\Entity\User 60 | { 61 | $uinfo = $this->entityManager->getRepository(\App\Entity\User::class)->findOneByUsername($uname); 62 | if ($uinfo === null) { 63 | return null; 64 | } 65 | if (! password_verify($pswd, $uinfo->getPassword())) { 66 | return null; 67 | } 68 | 69 | return $uinfo; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Controller/BlogController.php: -------------------------------------------------------------------------------- 1 | entityManager->getRepository(\App\Entity\Post::class)->find($id); 27 | } catch (\Exception $e) { 28 | throw new \Slim\Exception\HttpInternalServerErrorException($request, $e->getMessage()); 29 | } 30 | 31 | return $this->view->render($response, 'blog.twig', [ 'post' => $post ]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Controller/HealthController.php: -------------------------------------------------------------------------------- 1 | renderer->json($response, $this->health->status()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Controller/HomeController.php: -------------------------------------------------------------------------------- 1 | logger->info('Home page action dispatched'); 25 | 26 | return $this->view->render($response, 'index.twig'); 27 | } 28 | 29 | public function apiInfo(Request $request, Response $response): Response 30 | { 31 | return $this->view->render($response, 'api.twig'); 32 | } 33 | 34 | public function error(Request $request, Response $response): Response 35 | { 36 | $this->logger->info('Error log'); 37 | 38 | throw new \Slim\Exception\HttpInternalServerErrorException($request, 'Try error handler'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Entity/Post.php: -------------------------------------------------------------------------------- 1 | id; 31 | } 32 | public function setId(int $val): void 33 | { 34 | $this->id = $val; 35 | } 36 | public function getTitle(): ?string 37 | { 38 | return $this->title; 39 | } 40 | public function setTitle(?string $val): void 41 | { 42 | $this->title = $val; 43 | } 44 | public function getContent(): string 45 | { 46 | return $this->content; 47 | } 48 | public function setContent(string $val): void 49 | { 50 | $this->content = $val; 51 | } 52 | 53 | public function jsonSerialize(): mixed 54 | { 55 | return [ 56 | 'id' => $this->id, 57 | 'title' => $this->title, 58 | 'content' => $this->content, 59 | ]; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Entity/User.php: -------------------------------------------------------------------------------- 1 | id; 40 | } 41 | public function setId(int $val): void 42 | { 43 | $this->id = $val; 44 | } 45 | public function getUsername(): string 46 | { 47 | return $this->username; 48 | } 49 | public function setUsername(string $val): void 50 | { 51 | $this->username = $val; 52 | } 53 | public function getPassword(): string 54 | { 55 | return $this->password; 56 | } 57 | public function setPassword(string $val): void 58 | { 59 | $this->password = $val; 60 | } 61 | public function getFirstName(): ?string 62 | { 63 | return $this->firstName; 64 | } 65 | public function setFirstName(?string $val): void 66 | { 67 | $this->firstName = $val; 68 | } 69 | public function getLastName(): ?string 70 | { 71 | return $this->lastName; 72 | } 73 | public function setLastName(?string $val): void 74 | { 75 | $this->lastName = $val; 76 | } 77 | public function getEmail(): ?string 78 | { 79 | return $this->email; 80 | } 81 | public function setEmail(?string $val): void 82 | { 83 | $this->email = $val; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Middleware/BaseUrlMiddleware.php: -------------------------------------------------------------------------------- 1 | basePath = $app->getBasePath(); 29 | } 30 | 31 | /** 32 | * Invoke middleware. 33 | * 34 | * @param ServerRequestInterface $request The request 35 | * @param RequestHandlerInterface $handler The handler 36 | * 37 | * @return ResponseInterface The response 38 | */ 39 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 40 | { 41 | $baseUrl = $this->getBaseUrl($request); 42 | $this->view->getEnvironment()->addGlobal('base_url', $baseUrl); 43 | $request = $request->withAttribute('base_url', $baseUrl); 44 | 45 | return $handler->handle($request); 46 | } 47 | 48 | /** 49 | * Return the fully qualified base URL. 50 | * 51 | * Note that this method never includes a trailing / 52 | * 53 | * @param ServerRequestInterface $request The request 54 | * 55 | * @return string The base url 56 | */ 57 | public function getBaseUrl(ServerRequestInterface $request): string 58 | { 59 | $uri = $request->getUri(); 60 | $scheme = $uri->getScheme(); 61 | $authority = $uri->getAuthority(); 62 | $basePath = $this->basePath; 63 | 64 | if ($authority !== '' && ! str_starts_with($basePath, '/')) { 65 | $basePath .= '/' . $basePath; 66 | } 67 | 68 | return ($scheme !== '' ? $scheme . ':' : '') 69 | . ($authority ? '//' . $authority : '') 70 | . rtrim($basePath, '/'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Middleware/SessionMiddleware.php: -------------------------------------------------------------------------------- 1 | storage = &$_SESSION; 22 | } 23 | 24 | public function process(Request $request, RequestHandler $handler): Response 25 | { 26 | if (! isset($this->storage['logged'])) { 27 | $this->storage['logged'] = false; 28 | } 29 | 30 | $this->view->getEnvironment()->addGlobal('uinfo', array_key_exists('uinfo', $this->storage) ? $this->storage['uinfo'] : null); 31 | $request = $request->withAttribute('session', $this); 32 | $request = $request->withAttribute('uinfo', array_key_exists('uinfo', $this->storage) ? $this->storage['uinfo'] : null); 33 | return $handler->handle($request); 34 | } 35 | 36 | /** 37 | * ArrayAccess for storage 38 | */ 39 | public function offsetSet(mixed $offset, mixed $value): void 40 | { 41 | if (is_null($offset)) { 42 | $this->storage[] = $value; 43 | } else { 44 | $this->storage[$offset] = $value; 45 | } 46 | } 47 | public function offsetExists(mixed $offset): bool 48 | { 49 | return isset($this->storage[$offset]); 50 | } 51 | public function offsetUnset(mixed $offset): void 52 | { 53 | unset($this->storage[$offset]); 54 | } 55 | public function &offsetGet(mixed $offset): mixed 56 | { 57 | if ($this->offsetExists($offset)) { 58 | return $this->storage[$offset]; 59 | } 60 | return null; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Renderer/HtmlErrorRenderer.php: -------------------------------------------------------------------------------- 1 | getCode() === 404) { 24 | return $this->view->fetch('error/404.twig'); 25 | } 26 | 27 | if ($exception->getCode() === 0 || $exception->getCode() > 499) { 28 | if ($this->settings->get('debug')) { 29 | // We are in debug mode, and is not app exception so we let tracy manage the exception 30 | throw $exception; 31 | } 32 | if ($displayErrorDetails) { 33 | return $this->view->fetch('error/default.twig', [ 34 | 'title' => is_a($exception, '\Slim\Exception\HttpException') ? 35 | $exception->getTitle() : '500 - ' . $exception::class, 36 | 'debug' => $displayErrorDetails, 37 | 'type' => $exception::class, 38 | 'code' => $exception->getCode(), 39 | 'message' => $exception->getMessage(), 40 | 'file' => $exception->getFile(), 41 | 'line' => $exception->getLine(), 42 | 'trace' => $exception->getTraceAsString(), 43 | ]); 44 | } 45 | } 46 | 47 | return $this->view->fetch('error/default.twig', [ 48 | 'title' => is_a($exception, '\Slim\Exception\HttpException') ? 49 | $exception->getTitle() : '500 - ' . $exception::class, 50 | 'debug' => $displayErrorDetails, 51 | 'type' => $exception::class, 52 | 'code' => $exception->getCode(), 53 | 'message' => $exception->getMessage(), 54 | 'file' => $exception->getFile(), 55 | 'line' => $exception->getLine(), 56 | 'trace' => $exception->getTraceAsString(), 57 | ]); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Renderer/JsonErrorRenderer.php: -------------------------------------------------------------------------------- 1 | getCode() === 0 || $exception->getCode() > 499) { 16 | // We are in debug mode, and is not app exception so we let tracy manage the exception 17 | throw $exception; 18 | } 19 | 20 | if (($exception->getCode() >= 400 && $exception->getCode() <= 499) || 21 | ! $displayErrorDetails) { 22 | return json_encode([ 23 | 'message' => $exception->getMessage(), 24 | ]); 25 | } 26 | 27 | return json_encode([ 28 | 'title' => $exception::class, 29 | 'type' => $exception::class, 30 | 'message' => $exception->getMessage(), 31 | 'file' => $exception->getFile(), 32 | 'line' => $exception->getLine(), 33 | ]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Renderer/JsonRenderer.php: -------------------------------------------------------------------------------- 1 |
'
16 |             . json_encode($data, JSON_PRETTY_PRINT | JSON_NUMERIC_CHECK)
17 |             . '
'; 18 | 19 | $response->getBody()->write($body); 20 | 21 | return $response; 22 | } 23 | 24 | public function json( 25 | ResponseInterface $response, 26 | mixed $data = [], 27 | ): ResponseInterface { 28 | $response = $response->withHeader('Content-Type', 'application/json'); 29 | 30 | $response->getBody()->write( 31 | (string) json_encode( 32 | $data, 33 | JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR 34 | ) 35 | ); 36 | 37 | return $response; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Repository/PostRepository.php: -------------------------------------------------------------------------------- 1 | Returns an array containing all posts 19 | */ 20 | public function adminExport(): array 21 | { 22 | $result = $this->createQueryBuilder('a') 23 | ->orderBy('a.id', 'ASC') 24 | ->getQuery() 25 | ->getResult(); 26 | 27 | $data = [ 28 | 'title' => 'Blog Posts', 29 | 'columns' => [ 30 | [ 31 | 'field' => 'id', 32 | 'title' => 'Id', 33 | ], 34 | [ 35 | 'field' => 'title', 36 | 'title' => 'Title', 37 | ], 38 | [ 39 | 'field' => 'action', 40 | 'title' => '', 41 | ], 42 | ], 43 | 'data' => [], 44 | ]; 45 | foreach ($result as $row) { 46 | $data['data'][] = [ 47 | 'id' => $row->getId(), 48 | 'title' => $row->getTitle(), 49 | 'action' => 'View', 50 | ]; 51 | } 52 | 53 | return $data; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Repository/UserRepository.php: -------------------------------------------------------------------------------- 1 | Returns an array containing all posts 19 | */ 20 | public function adminExport(): array 21 | { 22 | $result = $this->createQueryBuilder('u') 23 | ->orderBy('u.id', 'ASC') 24 | ->getQuery() 25 | ->getResult(); 26 | 27 | $data = [ 28 | 'title' => 'User', 29 | 'columns' => [ 30 | [ 31 | 'field' => 'id', 32 | 'title' => 'Id', 33 | ], 34 | [ 35 | 'field' => 'username', 36 | 'title' => 'Username', 37 | ], 38 | [ 39 | 'field' => 'firstname', 40 | 'title' => 'Firstname', 41 | ], 42 | [ 43 | 'field' => 'lastname', 44 | 'title' => 'Lastname', 45 | ], 46 | [ 47 | 'field' => 'email', 48 | 'title' => 'EMail', 49 | ], 50 | ], 51 | 'data' => [], 52 | ]; 53 | foreach ($result as $row) { 54 | $data['data'][] = [ 55 | 'id' => $row->getId(), 56 | 'username' => $row->getUsername(), 57 | 'firstname' => $row->getFirstName(), 58 | 'lastname' => $row->getLastName(), 59 | 'email' => $row->getEmail(), 60 | ]; 61 | } 62 | 63 | return $data; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Services/Health.php: -------------------------------------------------------------------------------- 1 | Returns an array containing the service status information. 20 | */ 21 | public function status(): array 22 | { 23 | $now = new \DateTime(); 24 | return [ 25 | 'version' => $this->settings->get('version'), 26 | 'date' => $now->format(DATE_ISO8601), 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Services/Settings.php: -------------------------------------------------------------------------------- 1 | $settings An associative array of settings used to configure the instance. 13 | * The keys are strings, and the values can be of any type. 14 | */ 15 | public function __construct( 16 | private array $settings 17 | ) { 18 | } 19 | 20 | public function get(string $parents): mixed 21 | { 22 | $settings = $this->settings; 23 | $parents = explode('.', $parents); 24 | 25 | foreach ($parents as $parent) { 26 | if (is_array($settings) && (isset($settings[$parent]) || array_key_exists($parent, $settings))) { 27 | $settings = $settings[$parent]; 28 | } else { 29 | throw new \RuntimeException(sprintf('Trying to fetch invalid setting "%s"', implode('.', $parents))); 30 | } 31 | } 32 | 33 | return $settings; 34 | } 35 | 36 | public static function load(): self 37 | { 38 | $config = require self::getAppRoot() . '/config/settings/_base_.php'; 39 | 40 | foreach (glob(self::getAppRoot() . '/config/settings/*.php') as $file) { 41 | $key = basename($file, '.php'); 42 | if ($key === '_base_') { 43 | continue; 44 | } 45 | $config[$key] = require $file; 46 | } 47 | 48 | return new self($config); 49 | } 50 | 51 | public static function getAppRoot(): string 52 | { 53 | return dirname(__DIR__, 2); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tmpl/admin.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.twig' %} 2 | 3 | {% block contents %} 4 |

Administration

5 | 6 |

Database selection

7 |
8 |
9 | 12 |
13 |
14 | 15 |
16 |
17 | 18 |

19 | 20 |
21 | {% endblock %} 22 | 23 | 24 | {% block scripts %} 25 | 57 | {% endblock %} 58 | -------------------------------------------------------------------------------- /tmpl/api.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.twig' %} 2 | 3 | {% block contents %} 4 |

API

5 | 6 |
7 |

8 | This skeleton, provide an API exemple. 9 |

10 | 11 |

12 | API Routes 13 |

14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 33 | 36 | 37 | 38 | 41 | 42 | 45 | 48 | 49 | 50 | 53 | 54 | 57 | 65 | 66 | 67 | 70 | 71 | 74 | 77 | 78 | 79 |
MethodPathDescription
27 | GET 28 | 31 | /api/post 32 | 34 | Get All posts 35 |
39 | GET 40 | 43 | /api/post/{id} 44 | 46 | Get a post identified by is id 47 |
51 | POST 52 | 55 | /api/post 56 | 58 | Add a post, JSON body must be:
59 |
{
60 |   "title": "Blog Title",
61 |   "content": "Blog content."
62 | }
63 |                             
64 |
68 | DELETE 69 | 72 | /api/post/{id} 73 | 75 | Delete a post identified by is id 76 |
80 |
81 |
82 | {% endblock %} 83 | -------------------------------------------------------------------------------- /tmpl/blog.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.twig' %} 2 | 3 | {% block contents %} 4 |

Blog

5 | 6 |
7 | {% if post %} 8 |

{{ post.title }}

9 |
10 |

{{ post.content }}

11 | {% endif %} 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /tmpl/error/404.twig: -------------------------------------------------------------------------------- 1 | {% extends 'error/layout.twig' %} 2 | 3 | {% block contents %} 4 |

Slim4 Error

5 | 6 |

404

7 |
8 |

Page not found

9 | {% endblock %} 10 | 11 | -------------------------------------------------------------------------------- /tmpl/error/default.twig: -------------------------------------------------------------------------------- 1 | {% extends 'error/layout.twig' %} 2 | 3 | {% block contents %} 4 |

Slim4 Error

5 |

{{ title }}

6 | {% if debug %} 7 |

Details

8 |
    9 |
  • Type: {{ type }}
  • 10 |
  • Code: {{ code }}
  • 11 |
  • Message: {{ message }}
  • 12 |
  • File: {{ file }}
  • 13 |
  • Line: {{ line }}
  • 14 |
15 |

Trace

16 | {{ trace|nl2br }} 17 | {% endif %} 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /tmpl/error/layout.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | Error 13 | 14 | 15 | 16 | 17 | 26 | 27 |
28 | {% block contents %} 29 | {% endblock %} 30 |
31 | 32 | 33 | 34 | 37 | 40 | 43 | {% block scripts %}{% endblock %} 44 | 45 | 46 | -------------------------------------------------------------------------------- /tmpl/index.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.twig' %} 2 | 3 | {% block contents %} 4 |
5 |

6 | Slim 4 Skeleton 7 |

8 | 9 |

10 | This is a skeleton for a web application that uses the Slim 4 Framework. 11 |

12 | 13 |

14 | PSR-7 implementation 15 |

16 | 17 |

18 | By default this project uses the Slim-PSR-7 implementation. But you are 19 | free to use any other PSR-7 implementation. Just modify the composer.json 20 | file. 21 |

22 | 23 |

24 | Dependency injection 25 |

26 | 27 |

28 | By default this project uses the PHP-DI package to build a Container 29 | for dependency injection. 30 |

31 | 32 |

33 | Slim Twig extension 34 |

35 | 36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 49 | 52 | 53 | 54 | 57 | 60 | 61 | 62 | 65 | 68 | 69 | 70 | 73 | 76 | 77 | 78 | 81 | 84 | 85 | 86 |
CodeResult
47 | url_for('blog', {'id': '1'}) 48 | 50 | {{ url_for('blog', {'id': '1'}) }} 51 |
55 | full_url_for('home') 56 | 58 | {{ full_url_for('home') }} 59 |
63 | is_current_url('home') 64 | 66 | {{ is_current_url('home') ? 'TRUE' : 'FALSE' }} 67 |
71 | current_url() 72 | 74 | {{ current_url() }} 75 |
79 | get_uri().scheme 80 | 82 | {{ get_uri().scheme }} 83 |
87 |
88 | 89 |

90 | ORM 91 |

92 | 93 |

94 | By default this project uses dotrine ORM. 95 |

96 |
97 | {% endblock %} 98 | -------------------------------------------------------------------------------- /tmpl/layout.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{ pageTitle }} 15 | 16 | 17 | 18 |
19 | 93 | 94 |
95 | {% if flash %} 96 | {% for f in flash %} 97 | 101 | {% endfor %} 102 | {% endif %} 103 | 104 | {% block contents %} 105 | {% endblock %} 106 |
107 |
108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | {% block scripts %}{% endblock %} 116 | 117 | 118 | -------------------------------------------------------------------------------- /tmpl/login.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.twig' %} 2 | 3 | {% block contents %} 4 |

Login

5 |
6 |
7 |
8 | 9 |
10 | 11 |
12 |
13 |
14 | 15 |
16 | 17 |
18 |
19 | 20 |
21 |
22 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /var/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semhoun/slim-skeleton-mvc/f3af7544786422fbda06b85f4988d96f27743e7f/var/.gitkeep --------------------------------------------------------------------------------