├── docs ├── _sass │ ├── overrides.scss │ └── custom │ │ └── custom.scss ├── favicon.ico ├── assets │ ├── favicon-slim.fw.png │ ├── slim4-skeleton.fw.png │ └── slim4-skeleton-logo-only.fw.png ├── Gemfile ├── advanced.md ├── the-basics.md ├── getting-started.md ├── _config.yml ├── mvc2.planuml ├── image.md ├── index.md ├── links.md ├── translations.md ├── file-storage.md ├── request.planuml ├── frontend.md ├── uploading.md ├── routing.md ├── queues.md ├── mail.md ├── templates.md ├── logging.md ├── session.md ├── http-client.md ├── renderers.md ├── task-scheduling.md ├── validation.md ├── architecture.planuml ├── installation.md ├── cache.md ├── configuration.md ├── database.md ├── deployment.md ├── middleware.md ├── directory-structure.md ├── action.md ├── testing.md ├── console.md ├── domain.md └── security.md ├── logs └── .htaccess ├── tmp └── .htaccess ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── build.yml ├── .htaccess ├── public ├── index.php └── .htaccess ├── phpstan.neon ├── config ├── local.prod.php ├── routes.php ├── local.test.php ├── middleware.php ├── bootstrap.php ├── local.scrutinizer.php ├── local.dev.php ├── local.github.php ├── settings.php ├── defaults.php ├── env.example.php └── container.php ├── .phpstorm.meta.php ├── .editorconfig ├── CHANGELOG.md ├── phpcs.xml ├── src ├── Action │ └── Home │ │ ├── HomeAction.php │ │ └── PingAction.php ├── Renderer │ └── JsonRenderer.php └── Middleware │ └── ExceptionMiddleware.php ├── tests ├── TestCase │ └── Action │ │ └── Home │ │ ├── PingActionTest.php │ │ └── HomeActionTest.php └── Traits │ ├── AppTestTrait.php │ ├── ArrayTestTrait.php │ ├── ContainerTestTrait.php │ ├── HttpJsonTestTrait.php │ └── HttpTestTrait.php ├── .gitignore ├── LICENSE ├── phpunit.xml ├── .gitattributes ├── README.md ├── composer.json └── .cs.php /docs/_sass/overrides.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /logs/.htaccess: -------------------------------------------------------------------------------- 1 | deny from all -------------------------------------------------------------------------------- /tmp/.htaccess: -------------------------------------------------------------------------------- 1 | deny from all -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odan/slim4-skeleton/HEAD/docs/favicon.ico -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | ko_fi: dopitz 4 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteRule ^$ public/ [L] 3 | RewriteRule (.*) public/$1 [L] 4 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | run(); 4 | -------------------------------------------------------------------------------- /docs/_sass/custom/custom.scss: -------------------------------------------------------------------------------- 1 | $link-color: #719e40; 2 | 3 | a { 4 | color: $link-color; 5 | } 6 | -------------------------------------------------------------------------------- /docs/assets/favicon-slim.fw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odan/slim4-skeleton/HEAD/docs/assets/favicon-slim.fw.png -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "github-pages", group: :jekyll_plugins 4 | gem "just-the-docs" 5 | -------------------------------------------------------------------------------- /docs/assets/slim4-skeleton.fw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odan/slim4-skeleton/HEAD/docs/assets/slim4-skeleton.fw.png -------------------------------------------------------------------------------- /docs/advanced.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Advanced 4 | nav_order: 5 5 | has_children: true 6 | --- 7 | 8 | # Advanced 9 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/phpstan/phpstan/conf/bleedingEdge.neon 3 | parameters: 4 | level: 8 5 | paths: 6 | - src 7 | -------------------------------------------------------------------------------- /docs/the-basics.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: The Basics 4 | nav_order: 3 5 | has_children: true 6 | --- 7 | 8 | # The Basics 9 | -------------------------------------------------------------------------------- /docs/assets/slim4-skeleton-logo-only.fw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odan/slim4-skeleton/HEAD/docs/assets/slim4-skeleton-logo-only.fw.png -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Getting Started 4 | nav_order: 2 5 | has_children: true 6 | --- 7 | 8 | # Getting Started 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | # Redirect to front controller 2 | RewriteEngine On 3 | # RewriteBase / 4 | RewriteCond %{REQUEST_FILENAME} !-d 5 | RewriteCond %{REQUEST_FILENAME} !-f 6 | RewriteRule ^ index.php [QSA,L] -------------------------------------------------------------------------------- /config/local.prod.php: -------------------------------------------------------------------------------- 1 | get('/', \App\Action\Home\HomeAction::class)->setName('home'); 9 | $app->get('/ping', \App\Action\Home\PingAction::class); 10 | }; 11 | -------------------------------------------------------------------------------- /.phpstorm.meta.php: -------------------------------------------------------------------------------- 1 | '@'])); 6 | override( 7 | \Symfony\Component\Cache\Adapter\FilesystemAdapter::getItem(), 8 | type(\Psr\Cache\CacheItemPoolInterface::class) 9 | ); 10 | -------------------------------------------------------------------------------- /config/local.test.php: -------------------------------------------------------------------------------- 1 | addBodyParsingMiddleware(); 9 | $app->addRoutingMiddleware(); 10 | $app->add(BasePathMiddleware::class); 11 | $app->add(ExceptionMiddleware::class); 12 | }; 13 | -------------------------------------------------------------------------------- /config/bootstrap.php: -------------------------------------------------------------------------------- 1 | addDefinitions(__DIR__ . '/container.php') 11 | ->build(); 12 | 13 | // Create App instance 14 | return $container->get(App::class); 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `:package_name` will be documented in this file. 4 | 5 | Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) principles. 6 | 7 | ## NEXT - YYYY-MM-DD 8 | 9 | ### Added 10 | - Nothing 11 | 12 | ### Deprecated 13 | - Nothing 14 | 15 | ### Fixed 16 | - Nothing 17 | 18 | ### Removed 19 | - Nothing 20 | 21 | ### Security 22 | - Nothing 23 | -------------------------------------------------------------------------------- /config/local.scrutinizer.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ./src 11 | ./tests 12 | ./config 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Action/Home/HomeAction.php: -------------------------------------------------------------------------------- 1 | getBody()->write('Welcome!'); 13 | 14 | return $response; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /config/local.dev.php: -------------------------------------------------------------------------------- 1 | Controller: Request 15 | Controller -> Model: Invoke service 16 | Model -> Model: Business logic 17 | Model <-> Database: Data manipulation 18 | Model -> Controller: Service result 19 | Controller -> View: View data 20 | View -> Controller: Response 21 | Controller -> Browser: Response 22 | 23 | @enduml 24 | -------------------------------------------------------------------------------- /docs/image.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Image 4 | parent: Advanced 5 | --- 6 | 7 | # Image 8 | 9 | ## Image manipulation 10 | 11 | [Intervention Image](http://image.intervention.io/) is an open source PHP 12 | image handling and manipulation library. 13 | It provides an easier and expressive way to create, edit, 14 | and compose images and supports currently the two most common image 15 | processing libraries GD Library and Imagick. 16 | 17 | ## Read more 18 | 19 | * [Images](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 20 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Home 4 | nav_order: 1 5 | description: "Slim 4 Skeleton" 6 | --- 7 | 8 | ![image](https://user-images.githubusercontent.com/781074/67564463-4a102980-f723-11e9-9202-5e1d1641d06c.png) 9 | 10 | ## Introduction 11 | 12 | This is a Slim 4 Framework skeleton project 13 | for building awesome APIs, websites and web applications. 14 | 15 | ## Installation 16 | 17 | See [here](installation.md) 18 | 19 | ## Support 20 | 21 | * [Issues](https://github.com/odan/slim4-skeleton/issues) 22 | * [Donate](https://odan.github.io/donate.html) for this project. 23 | -------------------------------------------------------------------------------- /docs/links.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Links 4 | nav_order: 99 5 | --- 6 | 7 | # Links 8 | 9 | ## Slim 10 | 11 | * [Slim Framework Website](https://www.slimframework.com/) 12 | * [Slim Framework Documentation](https://www.slimframework.com/docs/v4/) 13 | * [Slim Framework Support Forum](https://discourse.slimframework.com/) 14 | 15 | ## Tutorials 16 | 17 | * [Slim Framework 4 Tutorial](https://odan.github.io/2019/11/05/slim4-tutorial.html) 18 | * [Slim Framework Articles](https://odan.github.io/) 19 | 20 | ## VSCode 21 | 22 | * [VSCode Snippets](https://gist.github.com/ybelenko/3b0b9d99404393dbd9bf1a474726c0b4) 23 | 24 | -------------------------------------------------------------------------------- /src/Action/Home/PingAction.php: -------------------------------------------------------------------------------- 1 | renderer = $renderer; 16 | } 17 | 18 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 19 | { 20 | return $this->renderer->json($response, ['success' => true]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docs/translations.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Translations 4 | published: true 5 | parent: Advanced 6 | --- 7 | 8 | # Translations 9 | 10 | ## Introduction 11 | 12 | There are good packages that allow you to add support for multiple languages to your application. 13 | 14 | ## Read more 15 | 16 | * [Translations with gettext](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 17 | * [Twig Translations](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 18 | * [Symfony Translations](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 19 | * [Twig Reference](https://symfony.com/doc/current/reference/twig_reference.html#trans) 20 | -------------------------------------------------------------------------------- /src/Renderer/JsonRenderer.php: -------------------------------------------------------------------------------- 1 | withHeader('Content-Type', 'application/json'); 14 | 15 | $response->getBody()->write( 16 | (string)json_encode( 17 | $data, 18 | JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR 19 | ) 20 | ); 21 | 22 | return $response; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docs/file-storage.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: File Storage 4 | parent: Advanced 5 | --- 6 | 7 | # File Storage 8 | 9 | ## Introduction 10 | 11 | The [Flysystem](https://flysystem.thephpleague.com/) PHP package, 12 | by Frank de Jonge, provides a powerful filesystem abstraction. 13 | There are also a lot of adapters available for working with 14 | SFTP, Azure, Google Cloud, Amazon S3 etc. 15 | 16 | ## Read more 17 | 18 | * [League Flysystem v3](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 19 | * [League Flysystem v3 SFTP](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 20 | * [League Flysystem v3 AWS S3](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 21 | -------------------------------------------------------------------------------- /docs/request.planuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | title Request and Response 4 | header v2022.05.19.0 5 | 6 | autonumber 7 | 8 | actor Browser 9 | participant Action 10 | participant Domain 11 | participant Renderer 12 | database Database 13 | 14 | Browser -> Action: Request 15 | Action -> Action: Collect input 16 | Action -> Domain: Invoke service 17 | Domain -> Domain: Validation, Complex logic 18 | Domain <-> Database: Read / write records 19 | Domain -> Domain: Build domain result 20 | Domain -> Action: Domain result 21 | Action -> Renderer: View data (domain result) 22 | Renderer -> Renderer : Render to JSON, HTML 23 | Renderer -> Action: Response 24 | Action -> Browser: Response 25 | 26 | @enduml 27 | -------------------------------------------------------------------------------- /config/settings.php: -------------------------------------------------------------------------------- 1 | createRequest('GET', '/ping'); 19 | $response = $this->app->handle($request); 20 | 21 | $this->assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); 22 | $this->assertResponseContains('{"success":true}', $response); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docs/uploading.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Uploading 4 | parent: Advanced 5 | --- 6 | 7 | # Uploading 8 | 9 | ## Introduction 10 | 11 | In web applications, one of the most common use-cases for storing files 12 | is storing user uploaded files such as photos and documents. 13 | 14 | ## Uploading files using POST forms 15 | 16 | See here: [Uploading files using POST forms](https://www.slimframework.com/docs/v4/cookbook/uploading-files.html) 17 | 18 | ## Uploading files using Filepond 19 | 20 | With [Filepond](https://pqina.nl/filepond/) you can upload anything, from anywhere. 21 | 22 | * [File Uploads with FilePond](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 23 | 24 | ## Read more 25 | 26 | * [File Uploads and Testing](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 27 | -------------------------------------------------------------------------------- /tests/Traits/AppTestTrait.php: -------------------------------------------------------------------------------- 1 | setUpApp(); 23 | } 24 | 25 | protected function setUpApp(): void 26 | { 27 | $container = (new ContainerBuilder()) 28 | ->addDefinitions(__DIR__ . '/../../config/container.php') 29 | ->build(); 30 | 31 | $this->app = $container->get(App::class); 32 | 33 | $this->setUpContainer($container); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /docs/routing.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Routing 4 | parent: The Basics 5 | --- 6 | 7 | # Routing 8 | 9 | The Slim Framework handles the routing and delegates the request to the appropriate route handler. 10 | 11 | [Read more](http://www.slimframework.com/docs/v4/objects/routing.html) 12 | 13 | ## Routes 14 | 15 | All routes are defined in [config/routes.php](https://github.com/odan/slim4-skeleton/blob/master/config/routes.php). 16 | 17 | Each route will be defined by a method that corresponds to the HTTP verb. 18 | 19 | For example, a `GET` request is defined as follows: 20 | 21 | ```php 22 | $app->get('/users', \App\Action\Customer\CustomerFinderAction::class); 23 | ``` 24 | 25 | ## Route groups 26 | 27 | Route groups are good to organize routes into logical groups. [Read more](http://www.slimframework.com/docs/v4/objects/routing.html#route-groups) 28 | 29 | -------------------------------------------------------------------------------- /docs/queues.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Queues 4 | parent: Advanced 5 | --- 6 | 7 | # Queues 8 | 9 | ## Introduction 10 | 11 | While building your web application, you may have some tasks, 12 | such as parsing and storing an uploaded CSV file, 13 | that take too long to perform during a typical web request. 14 | Thankfully, the `php-amqplib/php-amqplib` component allows you to 15 | easily create queued jobs that may be processed in the background. 16 | By moving time intensive tasks to a queue, your application can 17 | respond to web requests with blazing speed and provide a better 18 | user experience to your customers. 19 | 20 | **Read more** 21 | 22 | * [Rabbit MQ](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 23 | * [RabbitMQ Tutorial for PHP](https://www.rabbitmq.com/tutorials/tutorial-one-php.html) 24 | * [The RabbitMQ website](https://www.rabbitmq.com/) (Website) 25 | -------------------------------------------------------------------------------- /docs/mail.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Mail 4 | parent: Advanced 5 | --- 6 | 7 | # Mail 8 | 9 | ## Introduction 10 | 11 | Sending email doesn't have to be complicated. 12 | 13 | The [Symfony Mailer](https://symfony.com/doc/current/mailer.html) 14 | provides a clean and simple email API. 15 | Mailer provide drivers for sending email via 16 | SMTP, Mailgun, Postmark, Amazon SES, and sendmail, 17 | allowing you to quickly get started sending mail through 18 | a local or cloud based service of your choice. 19 | 20 | Symfony’s Mailer & Mime components form a powerful system 21 | for creating and sending emails - complete with support for multipart messages, 22 | Twig integration, CSS inlining, file attachments, signed messages and a lot more. 23 | 24 | ## Read more 25 | 26 | * [Mailer](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 27 | * [Symfony Mailer Documentation](https://symfony.com/doc/current/mailer.html) 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | nbproject/ 3 | composer.phar 4 | composer.lock 5 | .DS_STORE 6 | cache.properties 7 | .php_cs.cache 8 | .vscode 9 | .phpunit.result.cache 10 | .phpunit.cache/ 11 | phpunit.xml.bak 12 | package-lock.json 13 | 14 | # keys 15 | *.pem 16 | *.key 17 | *.ppk 18 | *.p12 19 | *.pfx 20 | *.crt 21 | *.cer 22 | *.der 23 | 24 | vendor/ 25 | build/ 26 | node_modules/ 27 | 28 | .env 29 | public/.env 30 | 31 | env.php 32 | config/env.php 33 | 34 | #!public/assets/ 35 | #public/assets/* 36 | #!public/assets/.htaccess 37 | 38 | !tmp/ 39 | tmp/* 40 | !tmp/.htaccess 41 | 42 | !tmp/translations/ 43 | tmp/translations/* 44 | !tmp/translations/empty 45 | 46 | !tmp/assets/ 47 | tmp/assets/* 48 | !tmp/assets/empty 49 | 50 | !tmp/twig/ 51 | tmp/twig/* 52 | !tmp/twig/empty 53 | 54 | !tmp/routes/ 55 | tmp/routes/* 56 | !tmp/routes/empty 57 | 58 | !logs/ 59 | logs/* 60 | !logs/.htaccess 61 | 62 | !public/assets/ 63 | public/assets/* 64 | !public/assets/.htaccess 65 | 66 | -------------------------------------------------------------------------------- /docs/templates.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Templates 4 | published: true 5 | parent: Advanced 6 | --- 7 | 8 | # Templates 9 | 10 | All templates are stored in the directory: `templates/` 11 | 12 | You can install any compatible template engine like the [Twig View](https://github.com/slimphp/Twig-View) 13 | or the [PHP View](https://github.com/slimphp/PHP-View) package. 14 | 15 | ## Read more 16 | 17 | * [Blade](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 18 | * [HTMX](https://ko-fi.com/s/3698cf30f3) (Slim 4 - eBook Vol. 3) 19 | * [Latte](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 20 | * [Mustache](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 21 | * [PHP View Templates](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 22 | * [Plates](https://ko-fi.com/s/3698cf30f3) (Slim 4 - eBook Vol. 3) 23 | * [Smarty](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 24 | * [Twig](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 25 | -------------------------------------------------------------------------------- /docs/logging.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Logging 4 | published: true 5 | parent: Advanced 6 | --- 7 | 8 | # Logging 9 | 10 | ## Introduction 11 | 12 | To help you learn more about what's happening within your application, 13 | this Slim skeleton provides robust logging services that allow you to log messages to files, 14 | the system error log, and even to Slack to notify your entire team. 15 | 16 | This project uses the [Monolog](https://seldaek.github.io/monolog/) package, 17 | which provides support for a variety of powerful log handlers. 18 | 19 | ## Configuration 20 | 21 | The default settings are stored in `config/defaults.php`, `$settings['logger']` 22 | 23 | The directory for all log files is: `logs/` 24 | 25 | ## Read more 26 | 27 | * [Logging with Monolog](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 28 | * [Logging with Sentry](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 29 | * [Error Handling](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 30 | 31 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2026 odan 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 | -------------------------------------------------------------------------------- /config/defaults.php: -------------------------------------------------------------------------------- 1 | false, 19 | ]; 20 | 21 | // Logger settings 22 | $settings['logger'] = [ 23 | // Log file location 24 | 'path' => __DIR__ . '/../logs', 25 | // Default log level 26 | 'level' => Psr\Log\LogLevel::DEBUG, 27 | ]; 28 | 29 | // Database settings 30 | $settings['db'] = [ 31 | 'host' => 'localhost', 32 | 'encoding' => 'utf8mb4', 33 | 'collation' => 'utf8mb4_unicode_ci', 34 | // PDO options 35 | 'options' => [ 36 | PDO::ATTR_PERSISTENT => false, 37 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 38 | PDO::ATTR_EMULATE_PREPARES => true, 39 | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, 40 | ], 41 | ]; 42 | 43 | return $settings; 44 | -------------------------------------------------------------------------------- /docs/session.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Session 4 | published: true 5 | parent: Advanced 6 | --- 7 | 8 | # Session 9 | 10 | ## Introduction 11 | 12 | Since HTTP driven applications are stateless, 13 | sessions provide a way to store information about the user across multiple requests. 14 | That user information is typically placed in a persistent store / backend 15 | that can be accessed from subsequent requests. 16 | 17 | ## Packages 18 | 19 | * [odan/session](https://github.com/odan/session) - Sessions, Flash messages and middlewares 20 | * [RKA Slim Session Middleware](https://github.com/akrabat/rka-slim-session-middleware) - Session handler and middleware 21 | * [slim/flash](https://github.com/slimphp/Slim-Flash) - Slim Framework flash messages service provider 22 | * [middlewares/php-session](https://github.com/middlewares/php-session) - PHP Session middleware 23 | 24 | ## Read more 25 | 26 | * [odan/session](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 27 | * [RKA Slim Session Middleware](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 1) 28 | * [Flash Messages](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 29 | -------------------------------------------------------------------------------- /docs/http-client.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: HTTP Client 4 | parent: Advanced 5 | --- 6 | 7 | # HTTP Client 8 | 9 | ## Introduction 10 | 11 | The Guzzle HTTP client provides an API around the cURL and PHP's stream wrapper 12 | allowing you to quickly make outgoing HTTP requests to communicate with 13 | other web applications. 14 | 15 | Before getting started, you should ensure that you have installed 16 | the Guzzle package as a dependency of your application. 17 | 18 | You may install it via Composer: 19 | 20 | ``` 21 | composer require guzzlehttp/guzzle 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```php 27 | use GuzzleHttp\Client; 28 | 29 | $client = new Client([ 30 | // Base URI is used with relative requests 31 | 'base_uri' => 'https://httpbin.org/', 32 | 'timeout' => 10, 33 | ]); 34 | 35 | // Send a request to https://httpbin.org/anything 36 | $response = $client->request('GET', 'anything'); 37 | ``` 38 | 39 | ## Read more 40 | 41 | * [Guzzle Documentation](https://docs.guzzlephp.org/en/stable/quickstart.html) 42 | * [Guzzle](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 43 | * [Stripe](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 44 | -------------------------------------------------------------------------------- /tests/Traits/ArrayTestTrait.php: -------------------------------------------------------------------------------- 1 | $currentKey)) { 26 | $currentValue = $currentValue->$currentKey; 27 | continue; 28 | } 29 | if (isset($currentValue[$currentKey])) { 30 | $currentValue = $currentValue[$currentKey]; 31 | continue; 32 | } 33 | 34 | return $default; 35 | } 36 | 37 | // @phpstan-ignore-next-line 38 | return $currentValue === null ? $default : $currentValue; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | tests/TestCase 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | src 18 | 19 | 20 | bin 21 | build 22 | docs 23 | public 24 | resources 25 | templates 26 | tmp 27 | vendor 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /config/env.example.php: -------------------------------------------------------------------------------- 1 | createRequest('GET', '/'); 19 | $response = $this->app->handle($request); 20 | 21 | $this->assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); 22 | $this->assertResponseContains('Welcome!', $response); 23 | } 24 | 25 | public function testPageNotFound(): void 26 | { 27 | $request = $this->createRequest('GET', '/nada'); 28 | $response = $this->app->handle($request); 29 | 30 | $this->assertSame(StatusCodeInterface::STATUS_NOT_FOUND, $response->getStatusCode()); 31 | } 32 | 33 | public function testPageNotFoundJson(): void 34 | { 35 | $request = $this->createRequest('GET', '/nada') 36 | ->withHeader('Accept', 'application/json'); 37 | 38 | $response = $this->app->handle($request); 39 | 40 | $this->assertSame(StatusCodeInterface::STATUS_NOT_FOUND, $response->getStatusCode()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Traits/ContainerTestTrait.php: -------------------------------------------------------------------------------- 1 | container = $container; 31 | 32 | return; 33 | } 34 | 35 | throw new UnexpectedValueException('Container must be initialized'); 36 | } 37 | 38 | /** 39 | * Define an object or a value in the container. 40 | * 41 | * @param string $name The entry name 42 | * @param mixed $value The value 43 | * 44 | * @throws BadMethodCallException 45 | * 46 | * @return void 47 | */ 48 | protected function setContainerValue(string $name, mixed $value): void 49 | { 50 | if (isset($this->container) && method_exists($this->container, 'set')) { 51 | $this->container->set($name, $value); 52 | 53 | return; 54 | } 55 | 56 | throw new BadMethodCallException('This DI container does not support this feature'); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /docs/renderers.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Renderers 4 | parent: The Basics 5 | --- 6 | 7 | # Renderer 8 | 9 | This skeleton projects includes a number of 10 | built-in Renderer classes, that allow you to 11 | return responses with various media types. 12 | 13 | There is also support for defining your own custom renderers, 14 | which gives you the flexibility to design your own media types. 15 | 16 | ## JsonRenderer 17 | 18 | Renders the response data into JSON, using utf-8 encoding. 19 | 20 | Media type: `application/json` 21 | 22 | Methods: 23 | 24 | * `json` - Builds a JSON response 25 | 26 | ```php 27 | renderer = $renderer; 42 | } 43 | 44 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 45 | { 46 | return $this->renderer->json($response, ['success' => true]); 47 | } 48 | } 49 | ``` 50 | 51 | ## Building a filetype specific response 52 | 53 | * [Image files](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 54 | * [Excel files](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 55 | * [PDF files](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 56 | * [ZIP files](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 57 | * [TCPDF](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 58 | -------------------------------------------------------------------------------- /docs/task-scheduling.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Task Scheduling 4 | parent: Advanced 5 | --- 6 | 7 | # Task Scheduling 8 | 9 | ## Introduction to Cronjob Management 10 | 11 | Managing scheduled tasks on your server 12 | involved manually editing the cron configuration for each 13 | task. This method is inconvenient since it lacks 14 | source control and requires SSH access to the 15 | server for changes. However, using a task scheduler 16 | simplifies this by requiring just one crontab entry. 17 | 18 | ### Quick Start: Setting up the Scheduler 19 | 20 | To set up a task scheduler, you only need to add a single crontab entry. 21 | To run `bin/cronjob.php` every minute, append the following 22 | line to your server's crontab: 23 | 24 | ```bash 25 | * * * * * /usr/bin/php /var/www/example.com/bin/cronjob.php 1>> /dev/null 2>&1 26 | ``` 27 | 28 | Now your scheduler is operational, and you can add new jobs 29 | without modifying the crontab. 30 | 31 | ## Recommended Libraries for Cronjob Scheduling 32 | 33 | Here are some recommended libraries to define and manage your scheduled tasks: 34 | 35 | * [Cron/Cron](https://github.com/Cron/Cron) 36 | * [PHP Cron Scheduler](https://github.com/peppeocchi/php-cron-scheduler) 37 | * [Jobby](https://github.com/jobbyphp/jobby) 38 | 39 | ## Resource Locking 40 | 41 | Resource locks ensure exclusive access to a shared resource, 42 | preventing multiple instances of a cronjob or console command 43 | from running simultaneously. 44 | 45 | For implementing resource locks, 46 | consider using [The Lock Component](https://symfony.com/doc/current/components/lock.html) 47 | by Symfony. 48 | 49 | By employing the techniques outlined above, 50 | you can easily manage and coordinate your scheduled tasks. 51 | -------------------------------------------------------------------------------- /docs/validation.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Validation 4 | parent: Advanced 5 | --- 6 | 7 | # Validation 8 | 9 | There are different approaches to validate your application's incoming data. 10 | 11 | ## Form and JSON validation 12 | 13 | **Additional Resources** 14 | 15 | * [CakePHP Validation](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 16 | * [Symfony Validator](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 17 | * [Problem Details for HTTP API](https://ko-fi.com/s/3698cf30f3) (Slim 4 - eBook Vol. 3) 18 | 19 | ## OpenAPI-based Validation 20 | 21 | The [league/openapi-psr7-validator](https://github.com/thephpleague/openapi-psr7-validator) 22 | package can validate PSR-7 messages against OpenAPI (3.0.x) specifications expressed in YAML or JSON. 23 | 24 | **Additional Resources** 25 | 26 | * [OpenAPI Validation](https://ko-fi.com/s/3698cf30f3) (Slim 4 - eBook Vol. 3) 27 | 28 | ## JSON schema validation 29 | 30 | The [league/json-guard](https://json-guard.thephpleague.com/) package 31 | allows you to validate JSON data against a 32 | [JSON schema](https://json-schema.org/). 33 | 34 | ## XML validation 35 | 36 | The [DOMDocument::schemaValidate](https://www.php.net/manual/en/domdocument.schemavalidate.php) 37 | method is able to validate XML files against a XSD schema. 38 | 39 | **Additional Resources** 40 | 41 | * [XML Validation](https://ko-fi.com/s/3698cf30f3) (XML Validation (Slim 4 - eBook Vol. 3) 42 | 43 | ## Assertion-Based Validation 44 | 45 | For assertion based input/output validation, you may use: 46 | 47 | * [webmozart/assert](https://github.com/webmozart/assert) 48 | * [beberlei/assert](https://github.com/beberlei/assert) 49 | 50 | This provides a range of assertion methods for enhanced data validation. 51 | -------------------------------------------------------------------------------- /docs/architecture.planuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | title Web Application Architecture 4 | header v22.5.19.0 5 | footer Page %page% of %lastpage% 6 | 7 | autonumber 8 | 9 | actor User 10 | participant FrontController 11 | participant SlimApp 12 | participant Router 13 | participant Action 14 | participant Service 15 | participant Repository 16 | database Database 17 | participant Renderer 18 | participant Emitter 19 | 20 | User -> FrontController: HTTP Request\n/hello 21 | activate FrontController 22 | FrontController -> SlimApp: Invoke run 23 | activate SlimApp 24 | SlimApp -> SlimApp: Create PSR-7\nRequest and\nResponse object 25 | SlimApp -> Router: Find matching\nroute handler 26 | activate Router 27 | Router -> Action: Dispatch\nto handler 28 | deactivate Router 29 | activate Action 30 | Action -> Service: Fetch and pass\nparameters 31 | activate Service 32 | Service -> Service: Input validation 33 | Service -> Repository: Invoke 34 | activate Repository 35 | Repository --> Repository : Build SQL query 36 | Repository -> Database: Query / execute 37 | activate Database 38 | Database --> Repository: Return records\nor new ID 39 | deactivate Database 40 | Repository -> Service: Query result 41 | deactivate Repository 42 | Service -> Service: Calculation,\nTransactions,\nMapping 43 | Service -> Action: Domain result 44 | deactivate Service 45 | Action -> Renderer: Pass Domain result 46 | activate Renderer 47 | Renderer-> Renderer: Render data\nto JSON\nor HTML 48 | Renderer-[#0000FF]> SlimApp: Response 49 | deactivate Renderer 50 | deactivate Action 51 | SlimApp-[#0000FF]> Emitter: Emit Response 52 | activate Emitter 53 | Emitter -[#0000FF]> User: HTTP Response (header, body) 54 | deactivate Emitter 55 | deactivate SlimApp 56 | deactivate FrontController 57 | 58 | @enduml 59 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Installation 4 | parent: Getting Started 5 | nav_order: 1 6 | --- 7 | 8 | # Installation 9 | 10 | The Slim Framework is a popular PHP micro-framework 11 | for building web applications. To set up this Slim Framework 12 | skeleton project, you will need to have PHP and Composer 13 | installed on your system. 14 | 15 | **Step 1:** Create a new project: 16 | 17 | Open a terminal and navigate to the directory where you 18 | want to create your new Slim Framework project. 19 | 20 | Run the following Composer command to create a new project: 21 | 22 | ```shell 23 | composer create-project odan/slim4-skeleton [my-app-name] 24 | ``` 25 | 26 | Replace `[my-app-name]` with the desired name for your project. 27 | This will create a new directory with the specified name 28 | and install the required dependencies. 29 | 30 | **Step 2:** Set permissions *(Linux only)* 31 | 32 | ```bash 33 | cd my-app 34 | 35 | sudo chown -R www-data tmp/ 36 | sudo chown -R www-data logs/ 37 | 38 | sudo chmod -R g+w tmp/ 39 | sudo chmod -R g+w logs/ 40 | ``` 41 | 42 | **Step 3:** Start the internal webserver 43 | 44 | Once the installation is complete, navigate to the newly 45 | created directory and start the built-in PHP development 46 | server by running the following command: 47 | 48 | ``` 49 | composer start 50 | ``` 51 | 52 | This will start the development server on port 8080 of 53 | your local machine. You can now access your 54 | application by visiting 55 | in your web browser. 56 | 57 | **Note:** The PHP internal webserver is designed for 58 | application development, testing or application demonstrations. 59 | It is not intended to be a full-featured web server. 60 | It should not be used on a public network. 61 | -------------------------------------------------------------------------------- /docs/cache.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Cache 4 | parent: Advanced 5 | --- 6 | 7 | # Cache 8 | 9 | ## Introduction 10 | 11 | Some data retrieval or processing tasks performed by your 12 | application could be CPU intensive or take several seconds to complete. 13 | When this is the case, it is common to cache the retrieved data for a 14 | time, so it can be retrieved quickly on subsequent requests for the same data. 15 | 16 | ## HTTP Caching 17 | 18 | Slim uses the optional standalone [slimphp/Slim-HttpCache](https://github.com/slimphp/Slim-HttpCache) PHP component 19 | for HTTP caching. You can use this component to create and return responses that 20 | contain `Cache`, `Expires`, `ETag`, and `Last-Modified` headers that control 21 | when and how long application output is retained by client-side caches. You may have to set your php.ini setting "session.cache_limiter" to an empty string in order to get this working without interferences. 22 | 23 | * [HTTP Caching](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 24 | 25 | ## Storage Caching 26 | 27 | The cached data is usually stored in a very fast data store such as Memcached or Redis. 28 | Thankfully, the [laminas/laminas-cache](https://docs.laminas.dev/laminas-cache/) and 29 | [symfony/cache](https://symfony.com/doc/current/components/cache.html) 30 | components provides a [PSR-6](https://www.php-fig.org/psr/psr-6/) and 31 | [PSR-16](https://www.php-fig.org/psr/psr-16/) compliant API for various cache backends, allowing you to take advantage 32 | of their blazing fast data retrieval and speed up your web application. 33 | 34 | * [Symfony Cache](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 35 | * [Redis](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 36 | * [Memcached](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 37 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Path-based git attributes 2 | # https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html 3 | 4 | public/* linguist-vendored 5 | docs/* linguist-documentation 6 | 7 | # Set the default behavior, in case people don't have core.autocrlf set. 8 | # Git will always convert line endings to LF on checkout. You should use 9 | # this for files that must keep LF endings, even on Windows. 10 | * text eol=lf 11 | 12 | # ------------------------------------------------------------------------------ 13 | # All the files and directories that can be excluded from dist, 14 | # we could have a more clean vendor/ 15 | # 16 | # So when someone will install that package through with --prefer-dist option, 17 | # all the files and directories listed in .gitattributes file will be excluded. 18 | # This could have a big impact on big deployments and/or testing. 19 | # ------------------------------------------------------------------------------ 20 | 21 | # /.github export-ignore 22 | # /tests export-ignore 23 | # /build export-ignore 24 | # /docs export-ignore 25 | # /.cs.php export-ignore 26 | # /.editorconfig export-ignore 27 | # /.gitattributes export-ignore 28 | # /.gitignore export-ignore 29 | # /.scrutinizer.* export-ignore 30 | # /phpcs.xml export-ignore 31 | # /phpstan.neon export-ignore 32 | # /phpunit.xml export-ignore 33 | 34 | # Define binary file attributes. 35 | # - Do not treat them as text. 36 | # - Include binary diff in patches instead of "binary files differ." 37 | *.pdf binary 38 | *.mo binary 39 | *.gif binary 40 | *.ico binary 41 | *.jpg binary 42 | *.jpeg binary 43 | *.png binary 44 | *.zip binary 45 | *.gif binary 46 | *.ico binary 47 | *.phar binary 48 | *.gz binary 49 | *.otf binary 50 | *.eot binary 51 | *.svg binary 52 | *.ttf binary 53 | *.woff binary 54 | *.woff2 binary 55 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Configuration 4 | parent: Getting Started 5 | nav_order: 2 6 | --- 7 | 8 | # Configuration 9 | 10 | ## Configuration Directory 11 | 12 | The directory for the configuration files is: `config/` 13 | 14 | ## Configuration Environments 15 | 16 | A typical application begins with three environments: dev, prod and test. 17 | Each environment represents a way to execute the same codebase with 18 | different configuration. Each environment 19 | loads its own individual configuration files. 20 | 21 | These different files are organized by environment: 22 | 23 | * for the `dev` environment: `config/local.dev.php` 24 | * for the `prod` environment: `config/local.prod.php` 25 | * for the (phpunit) `test` environment: `config/local.test.php` 26 | 27 | The file `config/settings.php` is the main configuration file and combines 28 | the default settings with environment specific settings. 29 | 30 | The configuration files are loaded in this order: 31 | 32 | * Load default settings from: `config/defaults.php` 33 | 34 | * If the environment variable `APP_ENV` is defined, 35 | load the environment specific file, e.g. `config/local.{env}.php` 36 | 37 | * Load secret credentials (if file exists) from: 38 | * `config/env.php` 39 | * `config/../../env.php` 40 | 41 | To switch the environment you can change the `APP_ENV` environment variable. 42 | 43 | ```php 44 | $_ENV['APP_ENV'] = 'prod'; 45 | ``` 46 | 47 | ## Secret Credentials 48 | 49 | For security reasons, all secret values 50 | are stored in a file called: **`env.php`**. 51 | 52 | Create a copy of the file `config/env.example.php` and rename it to `config/env.php` 53 | 54 | The `env.php` file is generally kept out of version control 55 | since it can contain sensitive API keys and passwords. 56 | 57 | ## Read more 58 | 59 | * [Environments and Configuration](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 60 | -------------------------------------------------------------------------------- /docs/database.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Database 4 | nav_order: 6 5 | --- 6 | 7 | # Database 8 | 9 | You have the freedom to choose any database package. Some popular options are: 10 | 11 | * [CakePHP Query Builder](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 12 | * [Laminas Query Builder](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 13 | * [Doctrine DBAL](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 14 | * [Cycle Query Builder](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 15 | * [PDO](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 16 | * [Yii Database](https://ko-fi.com/s/3698cf30f3) (Slim 4 - eBook Vol. 3) 17 | 18 | ## Database configuration 19 | 20 | You can configure the database settings for each server environment. 21 | 22 | The default settings are stored in `config/defaults.php`, `$settings['db']` 23 | 24 | ## Read more 25 | 26 | * [Amazon S3](https://ko-fi.com/s/3698cf30f3) (Slim 4 - eBook Vol. 3) 27 | * [Amazon DynamoDB](https://ko-fi.com/s/3698cf30f3) (Slim 4 - eBook Vol. 3) 28 | * [Apache Cassandra](https://ko-fi.com/s/3698cf30f3) (Slim 4 - eBook Vol. 3) 29 | * [Apache Kafka](https://ko-fi.com/s/3698cf30f3) (Slim 4 - eBook Vol. 3) 30 | * [Couchbase](https://ko-fi.com/s/3698cf30f3) (Slim 4 - eBook Vol. 3) 31 | * [Elasticsearch](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 32 | * [Doctrine CouchDB](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 33 | * [Firebase Realtime Database](https://ko-fi.com/s/3698cf30f3) (Slim 4 - eBook Vol. 3) 34 | * [IBM DB2](https://ko-fi.com/s/3698cf30f3) (Slim 4 - eBook Vol. 3) 35 | * [Oracle Database](https://ko-fi.com/s/3698cf30f3) (Slim 4 - eBook Vol. 3) 36 | * [PostgreSQL](https://ko-fi.com/s/3698cf30f3) (Slim 4 - eBook Vol. 3) 37 | * [Microsoft SQL Server](https://ko-fi.com/s/3698cf30f3) (Slim 4 - eBook Vol. 3) 38 | * [Multiple database connections](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 39 | 40 | -------------------------------------------------------------------------------- /docs/deployment.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Deployment 4 | parent: Advanced 5 | --- 6 | 7 | # Deployment 8 | 9 | ## Introduction 10 | 11 | When you're ready to deploy your Slim application to production, 12 | there are some important things you can do to make sure your application 13 | is running as efficiently as possible. 14 | 15 | In this document, we'll cover some great starting points for 16 | making sure your Slim application is deployed properly. 17 | 18 | ## Optimization 19 | 20 | ### Autoloader Optimization 21 | 22 | When deploying to production, make sure that you are optimizing Composer's 23 | class autoloader map so Composer can quickly find the proper 24 | file to load for a given class: 25 | 26 | ``` 27 | composer install --optimize-autoloader --no-dev 28 | ``` 29 | 30 | In addition to optimizing the autoloader, 31 | you should always be sure to include a `composer.lock` file in 32 | your project's source control repository. 33 | Your project's dependencies can be installed much faster 34 | when a `composer.lock` file is present. 35 | 36 | ### Optimizing Configuration Loading 37 | 38 | When deploying your application to production, you should make sure that you 39 | enable caching to improve the performance. 40 | 41 | This process includes the caching of the routes and the html templates. 42 | 43 | ### Deploying With GitHub Actions 44 | 45 | [GitHub Actions](https://github.com/features/actions) offers a great 46 | way to build and deploy artifacts to your production servers 47 | on various infrastructure providers such as DigitalOcean, 48 | Linode, AWS, and more. 49 | 50 | If you prefer to build and deploy your applications on your 51 | own machine or infrastructure, you may also 52 | try [Apache Ant](https://ant.apache.org/), Phing or [Deployer](https://deployer.org/). 53 | 54 | ## Read more 55 | 56 | * [Apache Ant](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 57 | * [Phing](https://ko-fi.com/s/3698cf30f3) (Slim 4 - eBook Vol. 3) 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

Slim 4 Skeleton

6 | 7 |
8 | 9 | [![Latest Version on Packagist](https://img.shields.io/github/release/odan/slim4-skeleton.svg)](https://packagist.org/packages/odan/slim4-skeleton) 10 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) 11 | [![Build Status](https://github.com/odan/slim4-skeleton/workflows/build/badge.svg)](https://github.com/odan/slim4-skeleton/actions) 12 | [![Coverage Status](https://coveralls.io/repos/github/odan/slim4-skeleton/badge.svg)](https://coveralls.io/github/odan/slim4-skeleton) 13 | [![Total Downloads](https://img.shields.io/packagist/dt/odan/slim4-skeleton.svg)](https://packagist.org/packages/odan/slim4-skeleton/stats) 14 | 15 | This is a skeleton to quickly set up a new [Slim 4](https://www.slimframework.com/) application. 16 | 17 |
18 | 19 | ## Requirements 20 | 21 | * PHP 8.2 - 8.5 22 | 23 | ## Installation 24 | 25 | Read the **[documentation](https://odan.github.io/slim4-skeleton/installation.html)** 26 | 27 | ## Features 28 | 29 | This project is based on best practices and industry standards: 30 | 31 | * [Standard PHP package skeleton](https://github.com/php-pds/skeleton) 32 | * HTTP router (Slim) 33 | * HTTP message interfaces (PSR-7) 34 | * HTTP Server Request Handlers, Middleware (PSR-15) 35 | * Dependency injection container (PSR-11) 36 | * Autoloader (PSR-4) 37 | * Logger (PSR-3) 38 | * Code styles (PSR-12) 39 | * Single action controllers 40 | * Unit- and integration tests 41 | * Tested with [Github Actions](https://github.com/odan/slim4-skeleton/actions) 42 | * [PHPStan](https://github.com/phpstan/phpstan) 43 | 44 | ## Support 45 | 46 | * [Issues](https://github.com/odan/slim4-skeleton/issues) 47 | * [Blog](https://odan.github.io/) 48 | * [Donate](https://odan.github.io/donate.html) for this project. 49 | * [Slim 4 eBooks](https://odan.github.io/donate.html) 50 | 51 | ## License 52 | 53 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 54 | -------------------------------------------------------------------------------- /docs/middleware.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Middleware 4 | parent: The Basics 5 | --- 6 | 7 | # Middleware 8 | 9 | The Middleware concept provides a convenient mechanism for inspecting and filtering 10 | HTTP requests entering your application. 11 | 12 | For example, this Slim Skeleton project 13 | includes a middleware that verifies the user of your application is authenticated. 14 | If the user is not authenticated, the middleware will return a `401 Unauthorized` 15 | response. However, if the user is authenticated, the middleware will allow the 16 | request to proceed further into the application. 17 | 18 | ## Registering Middleware 19 | 20 | ### Global middleware 21 | 22 | If you want a middleware to run during every HTTP request to your application, 23 | list the middleware class in the file: 24 | [config/middleware.php](https://github.com/odan/slim4-skeleton/blob/master/config/middleware.php) 25 | 26 | ### Assigning Middleware To Routes 27 | 28 | If you would like to assign middleware to specific routes, 29 | you should first assign the middleware a key in `config/container.php`. 30 | 31 | You can add middleware to all routes, 32 | to a specific route or to a group of routes. 33 | This makes it easier to differentiate between public and protected areas, 34 | as well as API resources etc. 35 | 36 | Once the middleware has been defined in the DI container, 37 | you may use the `add` method to assign the middleware to a route 38 | using the fully qualified class name: 39 | 40 | ```php 41 | $app->get('/my-path', \App\Action\MyAction::class)->add(MyMiddleware::class); 42 | ``` 43 | 44 | Assigning middleware to a group of routes: 45 | 46 | ```php 47 | use Slim\Routing\RouteCollectorProxy; 48 | 49 | // ... 50 | 51 | $app->group( 52 | '/my-route-group', 53 | function (RouteCollectorProxy $app) { 54 | $app->get('/sub-resource', \App\Action\MyAction::class); 55 | // ... 56 | } 57 | )->add(MyMiddleware::class); 58 | ``` 59 | 60 | ## Read more 61 | 62 | * [Slim 4 - Middleware](https://www.slimframework.com/docs/v4/concepts/middleware.html) (Documentation) 63 | * [Slim 4 - Routing](https://www.slimframework.com/docs/v4/objects/routing.html) (Documentation) 64 | -------------------------------------------------------------------------------- /docs/directory-structure.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Directory Structure 4 | parent: Getting Started 5 | nav_order: 3 6 | --- 7 | 8 | # Directory structure 9 | 10 | The directory structure is based on the [Standard PHP package skeleton](https://github.com/php-pds/skeleton). 11 | 12 | The `public` directory in your project contains 13 | the front-controller `index.php` and other web accessible files 14 | such as images, CC and JavaScript files. 15 | 16 | The `src` directory contains the core code for your application. 17 | 18 | The `config` directory contains the application settings such as 19 | the routes, service container, database connection and so on. 20 | 21 | The `templates` directory contains the view templates 22 | for your application. You can use the Slim Framework's 23 | template engine, or you can use a third-party 24 | template engine such as Twig or Latte. 25 | 26 | {% raw %} 27 | ``` 28 | . 29 | ├── build # Compiled files (artifacts) 30 | ├── config # Configuration files 31 | ├── docs # Documentation files 32 | ├── logs # Log files 33 | ├── public # Web server files 34 | ├── resources # Other resource files 35 | │ ├── migrations # Database migration files 36 | │ ├── seeds # Data seeds 37 | │ └── translations # The .po message files for PoEdit 38 | ├── src # PHP source code (The App namespace) 39 | │ ├── Action # Controller actions (HTTP layer) 40 | │ ├── Console # Console commands 41 | │ ├── Domain # The core application 42 | │ ├── Renderer # Render and Url helper (HTTP layer) 43 | │ ├── Middleware # Middleware (HTTP layer) 44 | │ └── Support # Helper classes and functions 45 | ├── templates # HTML templates 46 | ├── tests # Automated tests 47 | ├── tmp # Temporary files 48 | ├── vendor # Reserved for composer 49 | ├── build.xml # Ant build tasks 50 | ├── composer.json # Project dependencies 51 | ├── LICENSE # The license 52 | └── README.md # This file 53 | ``` 54 | {% endraw %} 55 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "odan/slim4-skeleton", 3 | "description": "A Slim 4 skeleton", 4 | "license": "MIT", 5 | "type": "project", 6 | "keywords": [ 7 | "slim-framework", 8 | "skeleton", 9 | "slim", 10 | "slim4" 11 | ], 12 | "require": { 13 | "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", 14 | "ext-json": "*", 15 | "fig/http-message-util": "^1.1", 16 | "monolog/monolog": "^3", 17 | "nyholm/psr7": "^1.8.1", 18 | "nyholm/psr7-server": "^1.1", 19 | "php-di/php-di": "^7", 20 | "selective/basepath": "^2", 21 | "slim/slim": "^4" 22 | }, 23 | "require-dev": { 24 | "friendsofphp/php-cs-fixer": "^3", 25 | "phpstan/phpstan": "^2", 26 | "phpunit/phpunit": "^11", 27 | "selective/test-traits": "^4", 28 | "squizlabs/php_codesniffer": "^4" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "App\\": "src/" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "App\\Test\\": "tests/" 38 | } 39 | }, 40 | "config": { 41 | "process-timeout": 0, 42 | "sort-packages": true 43 | }, 44 | "scripts": { 45 | "cs:check": "php-cs-fixer fix --dry-run --format=txt --verbose --diff --config=.cs.php --ansi --allow-unsupported-php-version=yes", 46 | "cs:fix": "php-cs-fixer fix --config=.cs.php --ansi --verbose --allow-unsupported-php-version=yes", 47 | "sniffer:check": "phpcs --standard=phpcs.xml", 48 | "sniffer:fix": "phpcbf --standard=phpcs.xml", 49 | "stan": "phpstan analyse -c phpstan.neon --no-progress --ansi", 50 | "start": "php -S localhost:8080 -t public/", 51 | "test": "phpunit --configuration phpunit.xml --do-not-cache-result --colors=always --display-warnings --display-deprecations --no-coverage", 52 | "test:all": [ 53 | "@cs:check", 54 | "@sniffer:check", 55 | "@stan", 56 | "@test" 57 | ], 58 | "test:coverage": [ 59 | "@putenv XDEBUG_MODE=coverage", 60 | "phpunit --configuration phpunit.xml --do-not-cache-result --colors=always --display-warnings --display-deprecations --coverage-clover build/coverage/clover.xml --coverage-html build/coverage --coverage-text" 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | paths-ignore: 7 | - 'docs/**' 8 | 9 | jobs: 10 | run: 11 | runs-on: ${{ matrix.operating-system }} 12 | strategy: 13 | matrix: 14 | operating-system: [ ubuntu-latest ] 15 | php-versions: [ '8.2', '8.3', '8.4', '8.5' ] 16 | name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} 17 | env: 18 | APP_ENV: github 19 | 20 | services: 21 | mysql: 22 | image: mysql:8.0.23 23 | env: 24 | MYSQL_ROOT_PASSWORD: root 25 | MYSQL_DATABASE: test 26 | MYSQL_ALLOW_EMPTY_PASSWORD: true 27 | ports: 28 | - 33306:3306 29 | 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v5 33 | 34 | - name: Setup PHP 35 | uses: shivammathur/setup-php@v2 36 | with: 37 | php-version: ${{ matrix.php-versions }} 38 | extensions: mbstring, pdo, pdo_mysql, intl, zip 39 | 40 | - name: Check PHP version 41 | run: php -v 42 | 43 | - name: Check Composer version 44 | run: composer -V 45 | 46 | - name: Check PHP extensions 47 | run: php -m 48 | 49 | - name: Check MySQL version 50 | run: mysql -V 51 | 52 | - name: Start MySQL 53 | run: sudo systemctl start mysql 54 | 55 | - name: Check MySQL variables 56 | run: mysql -uroot -proot -e "SHOW VARIABLES LIKE 'version%';" 57 | 58 | - name: Create database 59 | run: mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS slim_skeleton_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;' 60 | 61 | - name: Validate composer.json and composer.lock 62 | run: composer validate 63 | 64 | - name: Install dependencies 65 | run: composer update --prefer-dist --no-progress --no-suggest 66 | 67 | - name: Run PHP Coding Standards Fixer 68 | run: composer cs:check 69 | 70 | - name: Run PHP CodeSniffer 71 | run: composer sniffer:check 72 | 73 | - name: Run PHPStan 74 | run: composer stan 75 | 76 | - name: Run tests 77 | if: ${{ matrix.php-versions != '8.4' }} 78 | run: composer test 79 | 80 | - name: Run tests with coverage 81 | if: ${{ matrix.php-versions == '8.4' }} 82 | run: composer test:coverage 83 | 84 | - name: Upload coverage 85 | if: ${{ matrix.php-versions == '8.4' }} 86 | uses: coverallsapp/github-action@v2 87 | with: 88 | github-token: ${{ secrets.GITHUB_TOKEN }} 89 | file: build/coverage/clover.xml 90 | -------------------------------------------------------------------------------- /docs/action.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Action 4 | parent: The Basics 5 | --- 6 | 7 | # Single Action Controller 8 | 9 | The *Action* does only these things: 10 | 11 | * It collects input from the HTTP request (if needed). 12 | * It invokes the **Domain** with those inputs (if required) and retains the result. 13 | * It builds an HTTP response (typically with the Domain results). 14 | 15 | All other logic, including all forms of input validation, error handling, and so on, 16 | are therefore pushed out of the Action and into the [Domain](domain.md) 17 | (for domain logic concerns), or the response [Renderer](renderers.md) 18 | (for presentation logic concerns). 19 | 20 | ### Request and Response 21 | 22 | Here is a brief overview of the typical application process that involves different participants: 23 | 24 | * The **Slim router and dispatcher** receives an HTTP request and dispatches it to an **Action**. 25 | 26 | * The **Action** invokes the **[Domain](domain.md)**, collecting any required inputs to the 27 | Domain from the HTTP request. 28 | 29 | * The **Action** then invokes the **[Renderer](renderers.md)** with the data 30 | it needs to build an HTTP Response. 31 | 32 | * The **Renderer** builds an HTTP response using the data fed to it by the **Action**. 33 | 34 | * The **Action** returns the HTTP response to the **Slim response emitter** and sends 35 | the HTTP Response. 36 | 37 | ![Request and Response](https://user-images.githubusercontent.com/781074/169254509-109925c4-c34d-49d3-98a1-76ab463e2234.png) 38 | 39 | ## Example 40 | 41 | ```php 42 | myService = $myService; 60 | $this->renderer = $renderer; 61 | } 62 | 63 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 64 | { 65 | // 1. collect input from the HTTP request (if needed) 66 | $data = (array)$request->getParsedBody(); 67 | 68 | // 2. Invokes the Domain (Application-Service) 69 | // with those inputs (if required) and retains the result 70 | $domainResult = $this->myService->doSomething($data); 71 | 72 | // 3. Build and return an HTTP response 73 | return $this->renderer->json($response, $domainResult); 74 | } 75 | } 76 | ``` 77 | -------------------------------------------------------------------------------- /tests/Traits/HttpJsonTestTrait.php: -------------------------------------------------------------------------------- 1 | createRequest($method, $uri); 31 | 32 | if ($data !== null) { 33 | $request->getBody()->write((string)json_encode($data)); 34 | } 35 | 36 | return $request->withHeader('Content-Type', 'application/json'); 37 | } 38 | 39 | /** 40 | * Verify that the specified array is an exact match for the returned JSON. 41 | * 42 | * @param array $expected The expected array 43 | * @param ResponseInterface $response The response 44 | * 45 | * @return void 46 | */ 47 | protected function assertJsonData(array $expected, ResponseInterface $response): void 48 | { 49 | $data = $this->getJsonData($response); 50 | 51 | $this->assertSame($expected, $data); 52 | } 53 | 54 | /** 55 | * Get JSON response as array. 56 | * 57 | * @param ResponseInterface $response The response 58 | * 59 | * @return array The data 60 | */ 61 | protected function getJsonData(ResponseInterface $response): array 62 | { 63 | $actual = (string)$response->getBody(); 64 | $this->assertJson($actual); 65 | 66 | return (array)json_decode($actual, true); 67 | } 68 | 69 | /** 70 | * Verify JSON response. 71 | * 72 | * @param ResponseInterface $response The response 73 | * 74 | * @return void 75 | */ 76 | protected function assertJsonContentType(ResponseInterface $response): void 77 | { 78 | $this->assertStringContainsString('application/json', $response->getHeaderLine('Content-Type')); 79 | } 80 | 81 | /** 82 | * Verify that the specified array is an exact match for the returned JSON. 83 | * 84 | * @param mixed $expected The expected value 85 | * @param string $path The array path 86 | * @param ResponseInterface $response The response 87 | * 88 | * @return void 89 | */ 90 | protected function assertJsonValue(mixed $expected, string $path, ResponseInterface $response): void 91 | { 92 | $this->assertSame($expected, $this->getArrayValue($this->getJsonData($response), $path)); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /docs/testing.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Testing 4 | nav_order: 8 5 | --- 6 | 7 | # Testing 8 | 9 | ## Usage 10 | 11 | The test directory is: `tests/` 12 | 13 | The fixture directory is: `tests/Fixture/` 14 | 15 | To start all tests, run: 16 | 17 | ``` 18 | composer test 19 | ``` 20 | 21 | To start all tests with code coverage, run: 22 | 23 | ``` 24 | composer test:coverage 25 | ``` 26 | 27 | The code coverage output directory is: `build/coverage/` 28 | 29 | ## Unit Tests 30 | 31 | Testing units in isolation of its dependencies. 32 | 33 | Unit tests should test the behavior and not the implementation details of your classes. 34 | Make sure that unit tests are running in-memory only, because they have to be very fast. 35 | 36 | ## HTTP Tests 37 | 38 | The `AppTestTrait` provides methods for making HTTP requests to your 39 | Slim application and examining the output. 40 | 41 | ### Creating a request 42 | 43 | Creating a `GET` request: 44 | 45 | ```php 46 | $request = $this->createRequest('GET', '/users'); 47 | ``` 48 | 49 | Creating a `POST` request: 50 | 51 | ```php 52 | $request = $this->createRequest('POST', '/users'); 53 | ``` 54 | 55 | Creating a JSON `application/json` request with payload: 56 | 57 | ```php 58 | $request = $this->createJsonRequest('POST', '/users', ['name' => 'Sally']); 59 | ``` 60 | 61 | Creating a form `application/x-www-form-urlencoded` request with payload: 62 | 63 | ```php 64 | $request = $this->createFormRequest('POST', '/users', ['name' => 'Sally']); 65 | ``` 66 | 67 | ### Creating a query string 68 | 69 | The `withQueryParams` method can generate 70 | URL-encoded query strings. Example: 71 | 72 | ```php 73 | $params = [ 74 | 'limit' => 10, 75 | ]; 76 | 77 | $request = $this->createRequest('GET', '/users'); 78 | 79 | // /users?limit=10 80 | $request = $request->withQueryParams($params); 81 | ``` 82 | 83 | ### Add BasicAuth to the request 84 | 85 | ```php 86 | $credentials = base64_encode('username:password'); 87 | $request = $request->withHeader('Authorization', sprintf('Basic %s', $credentials)); 88 | ``` 89 | 90 | ### Invoking a request 91 | 92 | The Slim App `handle()` method traverses the application 93 | middleware stack + actions handler and returns the Response object. 94 | 95 | ```php 96 | $response = $this->app->handle($request); 97 | ``` 98 | 99 | Asserting the HTTP status code: 100 | 101 | ```php 102 | $this->assertSame(200, $response->getStatusCode()); 103 | ``` 104 | 105 | Asserting a JSON response: 106 | 107 | ```php 108 | $this->assertJsonContentType($response); 109 | ``` 110 | 111 | Asserting JSON response data: 112 | 113 | ```php 114 | $expected = [ 115 | 'user_id' => 1, 116 | 'username' => 'admin', 117 | 'first_name' => 'John', 118 | 'last_name' => 'Doe', 119 | 'email' => 'john.doe@example.com', 120 | ]; 121 | 122 | $this->assertJsonData($expected, $response); 123 | ``` 124 | 125 | You can find more examples in: `tests/TestCase/Action/` 126 | 127 | ## Read more 128 | 129 | * [Testing with PHPUnit](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 130 | -------------------------------------------------------------------------------- /docs/console.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Console 4 | parent: Advanced 5 | --- 6 | 7 | # Console 8 | 9 | ## Installation 10 | 11 | You'll need to install the Symfony Console component to add command-line capabilities to your project. 12 | 13 | Use Composer to do this: 14 | 15 | ``` 16 | composer require symfony/console 17 | ``` 18 | 19 | ## Creating a console command 20 | 21 | Create a new command class, e.g. `src/Console/ExampleCommand.php` and copy/paste this content: 22 | 23 | ```php 24 | setName('example'); 39 | $this->setDescription('A sample command'); 40 | } 41 | 42 | protected function execute(InputInterface $input, OutputInterface $output): int 43 | { 44 | $output->writeln(sprintf('Hello, World!')); 45 | 46 | // The error code, 0 on success 47 | return 0; 48 | } 49 | } 50 | ``` 51 | 52 | ## Register the Console Application 53 | 54 | To integrate the Console application with your application, 55 | you'll need to register it. Create a file, e.g., `bin/console.php`, and add the following code 56 | 57 | ```php 58 | getParameterOption(['--env', '-e'], 'dev'); 69 | 70 | if ($env) { 71 | $_ENV['APP_ENV'] = $env; 72 | } 73 | 74 | /** @var ContainerInterface $container */ 75 | $container = (new ContainerBuilder()) 76 | ->addDefinitions(__DIR__ . '/../config/container.php') 77 | ->build(); 78 | 79 | try { 80 | /** @var Application $application */ 81 | $application = $container->get(Application::class); 82 | 83 | // Register your console commands here 84 | $application->add($container->get(ExampleCommand::class)); 85 | 86 | exit($application->run()); 87 | } catch (Throwable $exception) { 88 | echo $exception->getMessage(); 89 | exit(1); 90 | } 91 | 92 | ``` 93 | 94 | Set permissions: 95 | 96 | ```php 97 | chmod +x bin/console.php 98 | ``` 99 | 100 | To start to example command, run: 101 | 102 | ``` bash 103 | php bin/console.php example 104 | ``` 105 | 106 | The output: 107 | 108 | ``` 109 | Hello, World! 110 | ``` 111 | 112 | ## Console Commands 113 | 114 | To list all available commands, run: 115 | 116 | ``` bash 117 | php bin/console.php 118 | ``` 119 | 120 | ## Read more 121 | 122 | * [Symfony Console Commands](https://symfony.com/doc/current/console.html) 123 | * [Console](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 124 | -------------------------------------------------------------------------------- /tests/Traits/HttpTestTrait.php: -------------------------------------------------------------------------------- 1 | container instanceof ContainerInterface) { 35 | throw new RuntimeException('DI container not found'); 36 | } 37 | 38 | $factory = $this->container->get(ServerRequestFactoryInterface::class); 39 | 40 | return $factory->createServerRequest($method, $uri, $serverParams); 41 | } 42 | 43 | /** 44 | * Create a form request. 45 | * 46 | * @param string $method The HTTP method 47 | * @param string|UriInterface $uri The URI 48 | * @param array|null $data The form data 49 | * 50 | * @return ServerRequestInterface The request 51 | */ 52 | protected function createFormRequest(string $method, string|UriInterface $uri, ?array $data = null): ServerRequestInterface 53 | { 54 | $request = $this->createRequest($method, $uri); 55 | 56 | if ($data !== null) { 57 | $request = $request->withParsedBody($data); 58 | } 59 | 60 | return $request->withHeader('Content-Type', 'application/x-www-form-urlencoded'); 61 | } 62 | 63 | /** 64 | * Create a new response. 65 | * 66 | * @param int $code HTTP status code; defaults to 200 67 | * @param string $reasonPhrase Reason phrase to associate with status code 68 | * 69 | * @throws RuntimeException 70 | * 71 | * @return ResponseInterface The response 72 | */ 73 | protected function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface 74 | { 75 | if (!$this->container instanceof ContainerInterface) { 76 | throw new RuntimeException('DI container not found'); 77 | } 78 | 79 | $factory = $this->container->get(ResponseFactoryInterface::class); 80 | 81 | return $factory->createResponse($code, $reasonPhrase); 82 | } 83 | 84 | /** 85 | * Assert that the response body contains a string. 86 | * 87 | * @param string $expected The expected string 88 | * @param ResponseInterface $response The response 89 | * 90 | * @return void 91 | */ 92 | protected function assertResponseContains(string $expected, ResponseInterface $response): void 93 | { 94 | $body = (string)$response->getBody(); 95 | 96 | $this->assertStringContainsString($expected, $body); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /config/container.php: -------------------------------------------------------------------------------- 1 | fn() => require __DIR__ . '/settings.php', 24 | 25 | App::class => function (ContainerInterface $container) { 26 | $app = AppFactory::createFromContainer($container); 27 | 28 | // Register routes 29 | (require __DIR__ . '/routes.php')($app); 30 | 31 | // Register middleware 32 | (require __DIR__ . '/middleware.php')($app); 33 | 34 | return $app; 35 | }, 36 | 37 | // HTTP factories 38 | ResponseFactoryInterface::class => function (ContainerInterface $container) { 39 | return $container->get(Psr17Factory::class); 40 | }, 41 | 42 | ServerRequestFactoryInterface::class => function (ContainerInterface $container) { 43 | return $container->get(Psr17Factory::class); 44 | }, 45 | 46 | StreamFactoryInterface::class => function (ContainerInterface $container) { 47 | return $container->get(Psr17Factory::class); 48 | }, 49 | 50 | UploadedFileFactoryInterface::class => function (ContainerInterface $container) { 51 | return $container->get(Psr17Factory::class); 52 | }, 53 | 54 | UriFactoryInterface::class => function (ContainerInterface $container) { 55 | return $container->get(Psr17Factory::class); 56 | }, 57 | 58 | // The Slim RouterParser 59 | RouteParserInterface::class => function (ContainerInterface $container) { 60 | return $container->get(App::class)->getRouteCollector()->getRouteParser(); 61 | }, 62 | 63 | BasePathMiddleware::class => function (ContainerInterface $container) { 64 | return new BasePathMiddleware($container->get(App::class)); 65 | }, 66 | 67 | LoggerInterface::class => function (ContainerInterface $container) { 68 | $settings = $container->get('settings')['logger']; 69 | $logger = new Logger('app'); 70 | 71 | $filename = sprintf('%s/app.log', $settings['path']); 72 | $level = $settings['level']; 73 | $rotatingFileHandler = new RotatingFileHandler($filename, 0, $level, true, 0777); 74 | $rotatingFileHandler->setFormatter(new LineFormatter(null, null, false, true)); 75 | $logger->pushHandler($rotatingFileHandler); 76 | 77 | return $logger; 78 | }, 79 | 80 | ExceptionMiddleware::class => function (ContainerInterface $container) { 81 | $settings = $container->get('settings')['error']; 82 | 83 | return new ExceptionMiddleware( 84 | $container->get(ResponseFactoryInterface::class), 85 | $container->get(JsonRenderer::class), 86 | $container->get(LoggerInterface::class), 87 | (bool)$settings['display_error_details'], 88 | ); 89 | }, 90 | ]; 91 | -------------------------------------------------------------------------------- /.cs.php: -------------------------------------------------------------------------------- 1 | setUsingCache(false) 7 | ->setRiskyAllowed(true) 8 | ->setRules( 9 | [ 10 | '@PSR1' => true, 11 | '@PSR2' => true, 12 | // custom rules 13 | 'psr_autoloading' => true, 14 | 'align_multiline_comment' => ['comment_type' => 'phpdocs_only'], // psr-5 15 | 'phpdoc_to_comment' => false, 16 | 'no_superfluous_phpdoc_tags' => false, 17 | 'array_indentation' => true, 18 | 'array_syntax' => ['syntax' => 'short'], 19 | 'cast_spaces' => ['space' => 'none'], 20 | 'concat_space' => ['spacing' => 'one'], 21 | 'compact_nullable_type_declaration' => true, 22 | 'declare_equal_normalize' => ['space' => 'single'], 23 | 'general_phpdoc_annotation_remove' => [ 24 | 'annotations' => [ 25 | 'author', 26 | 'package', 27 | ], 28 | ], 29 | 'increment_style' => ['style' => 'post'], 30 | 'list_syntax' => ['syntax' => 'short'], 31 | 'echo_tag_syntax' => ['format' => 'long'], 32 | 'phpdoc_add_missing_param_annotation' => ['only_untyped' => false], 33 | 'phpdoc_align' => false, 34 | 'phpdoc_no_empty_return' => false, 35 | 'phpdoc_order' => true, // psr-5 36 | 'phpdoc_no_useless_inheritdoc' => false, 37 | 'protected_to_private' => false, 38 | 'yoda_style' => [ 39 | 'equal' => false, 40 | 'identical' => false, 41 | 'less_and_greater' => false 42 | ], 43 | 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], 44 | 'ordered_imports' => [ 45 | 'sort_algorithm' => 'alpha', 46 | 'imports_order' => ['class', 'const', 'function'], 47 | ], 48 | 'single_line_throw' => false, 49 | 'declare_strict_types' => false, 50 | 'blank_line_between_import_groups' => true, 51 | 'fully_qualified_strict_types' => true, 52 | 'no_null_property_initialization' => false, 53 | 'nullable_type_declaration_for_default_null_value' => false, 54 | 'operator_linebreak' => [ 55 | 'only_booleans' => true, 56 | 'position' => 'beginning', 57 | ], 58 | 'global_namespace_import' => [ 59 | 'import_classes' => true, 60 | 'import_constants' => null, 61 | 'import_functions' => null 62 | ], 63 | 'class_definition' => [ 64 | 'space_before_parenthesis' => true, 65 | ], 66 | 'trailing_comma_in_multiline' => [ 67 | 'after_heredoc' => true, 68 | 'elements' => ['array_destructuring', 'arrays', 'match'] 69 | ], 70 | 'function_declaration' => [ 71 | 'closure_fn_spacing' => 'none', 72 | ] 73 | ] 74 | ) 75 | ->setFinder( 76 | PhpCsFixer\Finder::create() 77 | ->in(__DIR__ . '/src') 78 | ->in(__DIR__ . '/tests') 79 | ->in(__DIR__ . '/config') 80 | ->in(__DIR__ . '/public') 81 | ->name('*.php') 82 | ->ignoreDotFiles(true) 83 | ->ignoreVCS(true) 84 | ); 85 | -------------------------------------------------------------------------------- /docs/domain.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Domain 4 | parent: The Basics 5 | --- 6 | 7 | # Domain 8 | 9 | The Domain layer is the core of the application. 10 | 11 | ## Services 12 | 13 | Here is the right place for complex **business logic** e.g. calculation, validation, transaction handling, file creation etc. 14 | Business logic is a step-up on complexity over CRUD (Create, Read, Update and Delete) operations. 15 | A service can be called directly from the action handler, a service, the console and from a test. 16 | 17 | ## Domain vs. Infrastructure 18 | 19 | The infrastructure (layer) does not belong to the core application 20 | because it acts like an external consumer to talk to your system, 21 | for example the database, sending emails etc. 22 | 23 | An Infrastructure service can be: 24 | 25 | * Implementations for boundary objects, e.g. the repository classes (communication with the database) 26 | * Web controllers (actions), console, etc. 27 | * Framework-specific code 28 | 29 | By separating domain from infrastructure code you automatically **increase testability** 30 | because you can replace the implementation by changing the adapter without affecting 31 | the interface users. 32 | 33 | Within the Domain layer you have multiple other types of classes, for example: 34 | 35 | * Services with the business logic, aka. Use cases 36 | * Value Objects, DTOs, Entities, aka. Model 37 | * The repository (interfaces), for boundary objects to the infrastructure. 38 | 39 | ## Keep it clean 40 | 41 | Most people may think that this pattern is not suitable because it results in too many files. 42 | That this will result in more files is true, however these files are very small and focus on 43 | exactly one specific task. You get very specific classes with only one clearly defined responsibility 44 | (see SRP of SOLID). So you should not worry too much about too many files, instead you should worry 45 | about too few and big files (fat controllers) with too many responsibilities. 46 | 47 | ## Read more 48 | 49 | This architecture was inspired by the following resources and books: 50 | 51 | * [The Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) 52 | * [The Onion Architecture](https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/) 53 | * [Action Domain Responder](https://github.com/pmjones/adr) 54 | * [Domain-Driven Design](https://amzn.to/3cNq2jV) (The blue book) 55 | * [Implementing Domain-Driven Design](https://amzn.to/2zrGrMm) (The red book) 56 | * [Hexagonal Architecture](https://fideloper.com/hexagonal-architecture) 57 | * [Alistair in the Hexagone](https://www.youtube.com/watch?v=th4AgBcrEHA) 58 | * [Hexagonal Architecture demystified](https://madewithlove.be/hexagonal-architecture-demystified/) 59 | * [Functional architecture](https://www.youtube.com/watch?v=US8QG9I1XW0&t=33m14s) (Video) 60 | * [Object Design Style Guide](https://www.manning.com/books/object-design-style-guide?a_aid=object-design&a_bid=4e089b42) 61 | * [Advanced Web Application Architecture](https://leanpub.com/web-application-architecture/) (Book) 62 | * [Advanced Web Application Architecture](https://www.slideshare.net/matthiasnoback/advanced-web-application-architecture-full-stack-europe-2019) (Slides) 63 | * [The Beauty of Single Action Controllers](https://driesvints.com/blog/the-beauty-of-single-action-controllers) 64 | * [On structuring PHP projects](https://www.nikolaposa.in.rs/blog/2017/01/16/on-structuring-php-projects/) 65 | * [Standard PHP package skeleton](https://github.com/php-pds/skeleton) 66 | * [Services vs Objects](https://dontpaniclabs.com/blog/post/2017/10/12/services-vs-objects) 67 | * [Stop returning arrays, use objects instead](https://www.brandonsavage.net/stop-returning-arrays-use-objects-instead/) 68 | * [Data Transfer Objects - What Are DTOs](https://www.youtube.com/watch?v=35QmeoPLPOQ) 69 | * [SOLID](https://www.digitalocean.com/community/conceptual_articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design) 70 | -------------------------------------------------------------------------------- /src/Middleware/ExceptionMiddleware.php: -------------------------------------------------------------------------------- 1 | responseFactory = $responseFactory; 32 | $this->renderer = $jsonRenderer; 33 | $this->displayErrorDetails = $displayErrorDetails; 34 | $this->logger = $logger; 35 | } 36 | 37 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 38 | { 39 | try { 40 | return $handler->handle($request); 41 | } catch (Throwable $exception) { 42 | return $this->render($exception, $request); 43 | } 44 | } 45 | 46 | private function render( 47 | Throwable $exception, 48 | ServerRequestInterface $request, 49 | ): ResponseInterface { 50 | $httpStatusCode = $this->getHttpStatusCode($exception); 51 | $response = $this->responseFactory->createResponse($httpStatusCode); 52 | 53 | // Log error 54 | if (isset($this->logger)) { 55 | $this->logger->error( 56 | sprintf( 57 | '%s;Code %s;File: %s;Line: %s', 58 | $exception->getMessage(), 59 | $exception->getCode(), 60 | $exception->getFile(), 61 | $exception->getLine() 62 | ), 63 | $exception->getTrace() 64 | ); 65 | } 66 | 67 | // Content negotiation 68 | if (str_contains($request->getHeaderLine('Accept'), 'application/json')) { 69 | $response = $response->withAddedHeader('Content-Type', 'application/json'); 70 | 71 | // JSON 72 | return $this->renderJson($exception, $response); 73 | } 74 | 75 | // HTML 76 | return $this->renderHtml($response, $exception); 77 | } 78 | 79 | public function renderJson(Throwable $exception, ResponseInterface $response): ResponseInterface 80 | { 81 | $data = [ 82 | 'error' => [ 83 | 'message' => $exception->getMessage(), 84 | ], 85 | ]; 86 | 87 | return $this->renderer->json($response, $data); 88 | } 89 | 90 | public function renderHtml(ResponseInterface $response, Throwable $exception): ResponseInterface 91 | { 92 | $response = $response->withHeader('Content-Type', 'text/html'); 93 | 94 | $message = sprintf( 95 | "\n
Error %s (%s)\n
Message: %s\n
", 96 | $this->html((string)$response->getStatusCode()), 97 | $this->html($response->getReasonPhrase()), 98 | $this->html($exception->getMessage()), 99 | ); 100 | 101 | if ($this->displayErrorDetails) { 102 | $message .= sprintf( 103 | 'File: %s, Line: %s', 104 | $this->html($exception->getFile()), 105 | $this->html((string)$exception->getLine()) 106 | ); 107 | } 108 | 109 | $response->getBody()->write($message); 110 | 111 | return $response; 112 | } 113 | 114 | private function getHttpStatusCode(Throwable $exception): int 115 | { 116 | $statusCode = StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR; 117 | 118 | if ($exception instanceof HttpException) { 119 | $statusCode = $exception->getCode(); 120 | } 121 | 122 | if ($exception instanceof DomainException || $exception instanceof InvalidArgumentException) { 123 | $statusCode = StatusCodeInterface::STATUS_BAD_REQUEST; 124 | } 125 | 126 | return $statusCode; 127 | } 128 | 129 | private function html(string $text): string 130 | { 131 | return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /docs/security.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Security 4 | nav_order: 5 5 | --- 6 | 7 | # Security 8 | 9 | ## Basic Authentication 10 | 11 | [BasicAuth](https://en.wikipedia.org/wiki/Basic_access_authentication) 12 | is an authentication scheme built into the HTTP protocol. 13 | As long as the client transmits its data over **HTTPS**, 14 | it's a secure **authentication** mechanism. 15 | 16 | ``` 17 | Authorization: Basic YXBpLXVzZXI6c2VjcmV0 18 | ``` 19 | 20 | The [tuupola/slim-basic-auth](https://github.com/tuupola/slim-basic-auth) package implements HTTP Basic Authentication. 21 | 22 | * [Basic Authentication](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 23 | 24 | ## OAuth 2.0 25 | 26 | For **authorization**, you could consider to use [OAuth 2.0](https://oauth.net/2/) in combination with a signed [JSON Web Token](https://oauth.net/2/jwt/). 27 | 28 | The JWTs can be used as OAuth 2.0 [Bearer-Tokens](https://oauth.net/2/bearer-tokens/) to encode all relevant parts of an access token into the access token itself instead of having to store them in a database. 29 | 30 | Please note: [OAuth 2.0 is not an authentication protocol](https://oauth.net/articles/authentication/). 31 | 32 | Clients may use the **HTTP Basic authentication** scheme, as defined in [RFC2617](https://tools.ietf.org/html/rfc2617), 33 | to authenticate with the server. 34 | 35 | After successful authentication, the client sends its token within the `Authorization` request header: 36 | 37 | ``` 38 | Authorization: Bearer RsT5OjbzRn430zqMLgV3Ia 39 | ``` 40 | 41 | The [lcobucci/jwt](https://github.com/lcobucci/jwt) and 42 | [firebase/php-jwt](https://github.com/firebase/php-jwt) packages 43 | are a very good tools to work with JSON Web Tokens. 44 | 45 | * [Firebase JWT](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 46 | * [Mezzio OAuth2 Server](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 47 | * [JSON Web Token (JWT)](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 48 | * [OAuth Libraries for PHP](https://oauth.net/code/php/) 49 | * [Auth0 PHP SDK](https://auth0.com/docs/libraries/auth0-php) 50 | * [Stop using JWT for sessions](http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/) 51 | * [Swagger - OAuth 2.0](https://swagger.io/docs/specification/authentication/oauth2/) 52 | 53 | ## Cross-site Request Forgery (CSRF) Protection 54 | 55 | Cross-site request forgery (CSRF) is a web security vulnerability 56 | that tricks a victim's browser into performing unwanted 57 | actions on a web application where the user is authenticated, 58 | without their knowledge or consent. 59 | 60 | * [CSRF](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 61 | * [Slim Framework CSRF Protection](https://github.com/slimphp/Slim-Csrf) 62 | 63 | **SameSite Cookies** can be used for security purposes 64 | to prevent CSRF attacks, 65 | by controlling whether cookies are sent along with cross-site requests, 66 | thereby limiting the risk of third-party interference with 67 | the intended functioning of web applications. 68 | 69 | * [SameSite Cookies](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 70 | * [selective/samesite-cookie](https://github.com/selective-php/samesite-cookie) 71 | 72 | ## Cross-Origin Resource Sharing (CORS) 73 | 74 | Cross-Origin Resource Sharing (CORS) is a security feature 75 | implemented by web browsers that controls how web pages 76 | in one domain can request resources from another domain, 77 | aiming to safely enable interactions between different origins. 78 | 79 | * [Setting up CORS](https://www.slimframework.com/docs/v4/cookbook/enable-cors.html) 80 | * [CORS](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 81 | * [middlewares/cors](https://github.com/middlewares/cors) 82 | 83 | ## Cross Site Scripting (XSS) Prevention 84 | 85 | Cross-site Scripting (XSS) is a client-side code injection attack. 86 | The attacker aims to execute malicious scripts in a web browser of the 87 | victim by including malicious code in a legitimate web page or web application. 88 | 89 | To prevent XSS you can use an Auto-Escaping Template System such as Twig 90 | or by using libraries that are specifically designed to sanitize HTML input: 91 | 92 | * [laminas/laminas-escaper](https://github.com/laminas/laminas-escaper) 93 | * [Cross Site Scripting Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html) 94 | * [Cross-site Scripting (XSS)](https://www.acunetix.com/websitesecurity/cross-site-scripting/) 95 | * [XSS - Cross-site Scripting Protection](https://ko-fi.com/s/3698cf30f3) (Slim 4 - eBook Vol. 3) 96 | 97 | ## More Resources 98 | 99 | * [Mezzio OAuth2 Server](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 100 | * [PHP Middleware](https://github.com/php-middleware) 101 | * [middlewares/firewall](https://github.com/middlewares/firewall) 102 | * [PSR-15 HTTP Middlewares](https://github.com/middlewares) 103 | * [Shieldon - Web Application Firewall](https://ko-fi.com/s/3698cf30f3) (Slim 4 - eBook Vol. 3) 104 | * [Spam Protection](https://ko-fi.com/s/5f182b4b22) (Slim 4 - eBook Vol. 1) 105 | * [Symfony Rate Limiter](https://ko-fi.com/s/e592c10b5f) (Slim 4 - eBook Vol. 2) 106 | * [XSS - Cross-site Scripting Protection](https://ko-fi.com/s/3698cf30f3) (Slim 4 - eBook Vol. 3) 107 | --------------------------------------------------------------------------------