Compare commits

...
Sign in to create a new pull request.

179 commits
main ... master

Author SHA1 Message Date
3473429f58
start rework for 2025 2025-07-08 22:30:04 +02:00
f5c444d4c7 fix typos in chapters 15 to 17 2022-05-31 18:50:35 +02:00
ebbcc6e7f6 add chapter 12 solutions 2022-05-31 18:50:35 +02:00
f857fa4752 simplify chapter 12 2022-05-31 18:50:35 +02:00
350f8e18b9 add chapter 11 solutions 2022-05-31 18:50:35 +02:00
9b14e32639 update chapter 11 2022-05-31 18:50:35 +02:00
fcf5bf2653 update chapter 10 solutions 2022-05-31 18:50:35 +02:00
e91a166816 add solutions for chapter 10 2022-05-31 18:50:35 +02:00
d880aeb9a6 add small typo and wording improvements to chapters 9 and 10, update name of time service 2022-05-31 18:50:35 +02:00
6920ea390d add solutions for chapter 9 and fix urltypos 2022-05-31 18:50:35 +02:00
0b1fa54d49 fix type in DI chapter 2022-05-31 18:50:35 +02:00
62cff766db fix some typos 2022-05-31 18:50:35 +02:00
2fbbd082f2 remove implementation from app directory 2022-05-31 18:50:35 +02:00
572896685f update implementation of chapter 9 2022-05-31 18:50:35 +02:00
26aa4b1502 rename implementation 09-wip directory 2022-05-31 18:50:35 +02:00
5dc8ad38dd explain implementation of ad-hoc depencency container 2022-05-31 18:50:35 +02:00
2c3901e9f9 explain implementation of ad-hoc depencency container 2022-05-31 18:50:35 +02:00
988a532b78 wip: rewrite di chapter 2022-05-31 18:50:35 +02:00
a4f171b98c readd implementation folder 2022-05-31 18:50:35 +02:00
7ff078b16f fix wrong namespace in for laminas request in http chapter 2022-05-31 18:50:35 +02:00
6818179857 update ecs and rector config 2022-05-31 18:50:35 +02:00
997d160796 enable intl extension 2022-05-31 18:50:35 +02:00
3f832d7c94 update ecs config to newer version in devhelper chapter 2022-05-31 18:50:35 +02:00
63a5ae837c add rector to dev helpers 2022-05-31 18:50:35 +02:00
fde9b5c11e update development helpers chapter 2022-05-31 18:50:35 +02:00
e92869c00c update devhelpers to use ecs instead of phpcs and php-cs-fixer 2022-05-31 18:50:35 +02:00
99a31e45d0 Update author name in composer chapter 2022-05-31 18:50:35 +02:00
b109ee1378 bump memory in vagrantfile to 512mb 2022-05-31 18:50:35 +02:00
8f7d95d86f readability fixes in chapters 7 and 9 2022-05-31 18:50:35 +02:00
44c5e06996 disable composer timeout in development helpers chapter 2022-05-31 18:50:35 +02:00
e4fa8b8e42 some more typo and readability fixes 2022-05-31 18:50:35 +02:00
68d4abab8f fix some typos and link to a blogpost about middleware pattern 2022-05-31 18:50:35 +02:00
d81535c3c0 Changing port to 1235 to not clash with smtp default port 2022-05-31 18:50:35 +02:00
00c31259f8 prepare 2022-05-31 18:50:35 +02:00
ececd7dcb5 add perfomance chapters 2022-05-31 18:50:35 +02:00
9a1f78947b add chapter about data repositories, and start work on perfomance chapter 2022-05-31 18:50:35 +02:00
eb20213b94 add 'adding content' chapter 2022-05-31 18:50:35 +02:00
ab3227b75f update readme 2022-05-31 18:50:35 +02:00
b12cf019e7 asdf 2022-05-31 18:50:35 +02:00
48c9c9467d add data from work folder 2022-05-31 18:50:35 +02:00
Patrick Louys
97579d6d91
Merge pull request #66 from s-moon/master
Small typo
2018-09-25 11:43:26 +02:00
Stephen Moon
3d561cd012
Small typo 2018-08-09 10:43:52 +01:00
Patrick Louys
8736e5cecb
Update README.md 2017-12-08 20:39:00 +01:00
Patrick Louys
24193f92ab
Update to-be-continued.md 2017-12-08 20:36:52 +01:00
Patrick Louys
9652d05afe
Update to-be-continued.md 2017-12-08 20:32:21 +01:00
Patrick Louys
4688caf85e
Update to-be-continued.md 2017-12-08 20:32:06 +01:00
Patrick Louys
70e7c1e93d Merge pull request #59 from DemoniacDeath/patch-1
Update 04-http.md
2017-03-02 15:51:05 +01:00
Michael Skvortsov
2f4c1f044d Update 04-http.md
A typo fixed
2017-03-02 03:51:48 +02:00
Patrick Louys
dd0d96305f Update 11-page-menu.md 2016-11-02 13:14:13 +01:00
Patrick Louys
6f91572434 Update 11-page-menu.md 2016-11-02 13:07:37 +01:00
Patrick Louys
1e5cd00b03 Update 11-page-menu.md 2016-11-02 13:05:02 +01:00
Patrick Louys
9a7b586548 Update 11-page-menu.md 2016-11-02 13:03:57 +01:00
Patrick Louys
a864b2aad5 Update 12-frontend.md 2016-11-01 17:52:22 +01:00
Patrick Louys
7a9f6d84ee Update README.md 2016-11-01 17:52:04 +01:00
Patrick Louys
c80b25a34c Update 10-dynamic-pages.md 2016-11-01 17:24:08 +01:00
Patrick Louys
132fdf4e6e Update 10-dynamic-pages.md 2016-11-01 17:22:53 +01:00
Patrick Louys
57f6118308 Update 10-dynamic-pages.md 2016-11-01 17:17:52 +01:00
Patrick Louys
215c07c94a Update 10-dynamic-pages.md 2016-11-01 17:14:02 +01:00
Patrick Louys
20bd93a61c Update 10-dynamic-pages.md 2016-11-01 17:00:27 +01:00
Patrick Louys
ff9595d961 Update 10-dynamic-pages.md 2016-11-01 16:56:57 +01:00
Patrick Louys
6859223386 Update 09-templating.md 2016-11-01 16:51:33 +01:00
Patrick Louys
20e0f82b2a Code changes 2016-11-01 16:48:27 +01:00
Patrick Louys
32a9e94450 added strict mode 2016-11-01 16:30:51 +01:00
Patrick Louys
8d3b3ee1e3 Update 08-dependency-injector.md 2016-11-01 16:29:36 +01:00
Patrick Louys
d6a70e94c0 Update 07-inversion-of-control.md 2016-11-01 16:27:27 +01:00
Patrick Louys
31bbb0fff7 Improved sentences 2016-11-01 16:26:05 +01:00
Patrick Louys
d9b7885c43 Updated code 2016-11-01 16:23:28 +01:00
Patrick Louys
b2d2fee013 Update 05-router.md 2016-11-01 16:18:02 +01:00
Patrick Louys
4169afd542 Made code location more explicit 2016-11-01 16:01:35 +01:00
Patrick Louys
0430b7d94c Updated composer require 2016-11-01 15:59:17 +01:00
Patrick Louys
973631449f Rewrote some sentences 2016-11-01 15:55:40 +01:00
Patrick Louys
2c59ba04da Updated code example 2016-11-01 15:52:51 +01:00
Patrick Louys
86af38af20 Updated composer require 2016-11-01 15:49:27 +01:00
Patrick Louys
4a9f46a503 Adding vendor folder to gitignore 2016-11-01 15:44:31 +01:00
Patrick Louys
625bf8ff1f Added gitignore 2016-11-01 15:40:10 +01:00
Patrick Louys
ec3656b10d Updated PHP version 2016-11-01 15:35:51 +01:00
Patrick Louys
f9a3ccd4c3 Added strict mode 2016-11-01 15:33:18 +01:00
Patrick Louys
bff1918030 Updated PHP version requirement 2016-11-01 15:21:21 +01:00
Patrick Louys
c9df0411b4 Create LICENSE 2016-08-31 12:51:41 +02:00
Patrick Louys
4a47f05943 Merge pull request #48 from gourabnagDev/patch-1
Fixed Typo in line 38
2016-04-03 12:20:59 +02:00
Gourab Nag
8683cba065 Fixed Typo in line 38 2016-04-03 02:18:02 +05:30
Patrick Louys
232714c2f2 Merge pull request #42 from Danack/patch-1
Changed lines that said not to commit the lock file.
2015-12-21 09:23:46 +01:00
Danack
477489988b Changed lines that said not to commit the lock file.
Because not committing it is a bad idea.
2015-12-17 02:29:43 +00:00
Patrick Louys
114be10207 Merge pull request #41 from Steve-A-Orr/11-page-menu-share-substitution
Replace unknown menu reader.
2015-12-13 12:50:10 +01:00
Steven Orr
939914d973 Replace 'FileMenuReader' with correct reader.
Author intended on sharing 'ArrayMenuReader' not unknown 'FileMenuReader' with injector.
2015-12-12 23:37:09 -08:00
Steven Orr
ed960ddb7d Merge pull request #1 from Steve-A-Orr/11-page-menu-word-correction
Replace word 'order' with 'folder'.
2015-12-12 22:56:03 -08:00
Steven Orr
2e86565766 Replace word 'order' with 'folder'.
Author intended to instruct the creation of a new folder called 'Menu'.
2015-12-12 22:54:35 -08:00
Patrick Louys
8a45826033 Merge pull request #40 from kenjis/patch-1
Fix code highlight
2015-11-30 08:51:31 +01:00
kenjis
518e8687eb Fix code highlight 2015-11-29 19:44:09 +09:00
Patrick Louys
1b181f6806 Merge pull request #39 from HassanAlthaf/patch-1
Fixed an issue.
2015-11-26 08:46:48 +01:00
Hassan Althaf
e81e39c8ac Fixed an issue.
Fixed the issue stated in: https://github.com/PatrickLouys/no-framework-tutorial/issues/38
2015-11-26 11:51:22 +05:30
Patrick Louys
5c550f371b Merge pull request #36 from Zvax/dep-inj
#35 slightly more logical grouping
2015-11-12 14:13:19 +01:00
Félix Gagnon-Grenier
8272d5996e #35 slightly more logical grouping 2015-11-11 15:35:46 -05:00
Patrick Louys
fe2db7ed04 Merge pull request #32 from tsawler/master
Correct namespace
2015-09-25 12:19:07 +02:00
Trevor Sawler
72d542843d Correct namespace 2015-09-24 09:07:04 -03:00
Patrick
d1a3c011a4 fixed weird sentence 2015-09-23 20:23:35 +02:00
Patrick Louys
b83483d500 Merge pull request #26 from srph/patch-1
`interface` to `class`
2015-09-23 20:21:35 +02:00
Patrick
44c0bab268 spacing 2015-09-09 21:55:58 +02:00
Patrick
206e2ab45b name on next line 2015-09-09 21:55:29 +02:00
Patrick
196515f3bd tbc changed text 2015-09-09 21:54:53 +02:00
Patrick
98b1fd8ce7 tbc typo 2015-09-09 21:54:13 +02:00
Patrick
feb010bf3b frontend 2015-09-09 21:52:31 +02:00
Patrick
22acd87bf6 qMerge branch 'master' of https://github.com/PatrickLouys/no-framework-tutorial 2015-09-02 19:26:20 +02:00
Patrick
e36b01ba29 finished chapter 2015-09-02 19:25:43 +02:00
Patrick Louys
256f6f584d Merge pull request #29 from KevinMGranger/patch-1
Fix typo in 05-router.md
2015-07-24 12:02:39 +02:00
Kevin M Granger
f2c5137ae1 Fix typo in 05-router.md
Routers.php -> Routes.php
2015-07-23 15:09:52 -07:00
Patrick Louys
b0bf75da2b Merge pull request #25 from marcel-burkhard/patch-1
Update 08-dependency-injector.md
2015-07-18 17:59:59 +02:00
Kier Borromeo
f7344a1f88 interface to class
This seems to be a typo. No?
2015-04-14 15:54:42 +08:00
burki94
b1add84072 Update 08-dependency-injector.md
Auryn\Provider was replaced with Auryn\Injector
2015-04-06 13:12:37 +02:00
Patrick
86064842f4 fixed typo 2015-02-14 01:07:31 +01:00
Patrick
047f34863d frontend renderer 2015-02-13 16:36:27 +01:00
Patrick
dab90c5b24 frontend renderer 2015-02-13 16:36:04 +01:00
Patrick
24ca274836 added layout file 2015-02-13 14:48:12 +01:00
Patrick
589585cde4 added README entry for new chapter 2015-02-08 20:53:31 +01:00
Patrick
c9b9fb2d12 continued menu chapter 2015-02-08 20:51:22 +01:00
Patrick
20aef6e741 expanded menu chapter and refactored old chapters 2015-02-08 20:16:44 +01:00
Patrick
d9738776b2 added more to menu chapter 2015-02-08 18:00:41 +01:00
Patrick
53d5731ffa Merge branch 'master' of https://github.com/PatrickLouys/no-framework-tutorial 2015-01-28 22:27:21 +01:00
Patrick Louys
84ba9a1654 update header() call to fix overwrite header bug 2015-01-28 22:26:47 +01:00
Patrick
967bb2bdec begin next chapter 2015-01-21 23:45:56 +01:00
Patrick
31b9f9f57f explain __DIR__. solves #13 2015-01-14 21:16:39 +01:00
Patrick
79e64e3205 make version requirement more clear. resolves #12 2015-01-14 21:10:53 +01:00
Patrick
e23ef871c8 make version requirement more clear. resolves #12 2015-01-14 21:10:28 +01:00
Patrick
c646930728 make version requirement more clear. resolves #12 2015-01-14 21:09:38 +01:00
Patrick
03c012ece5 renamed method to readBySlug. closes #14 2015-01-14 21:04:27 +01:00
Patrick
a99778b049 rename engine to renderer, solves #15 2015-01-14 20:54:18 +01:00
Patrick Louys
521b4025c0 Merge pull request #21 from MadaraUchiha/master
Add leading zeros so that files list in correct order
2015-01-14 14:51:44 +01:00
Madara
c20b26c684 Add leading zeros so that files list in correct order
Fix all links to new file names
2015-01-14 15:46:29 +02:00
Patrick Louys
c9215d89d5 Merge pull request #17 from HassanAlthaf/patch-2
Update 9-templating.md
2014-12-08 12:21:28 +01:00
Patrick Louys
1ddfec5112 Merge pull request #19 from Hamz-a/master
#18 renamed $woops to $whoops in Error Handler
2014-12-08 12:20:07 +01:00
HamZa
d1b04a16df #18 changed $woops to $whoops 2014-12-08 11:53:46 +01:00
HamZa
0bf3f2d3ce Changed $woops to $whoops 2014-12-08 11:42:21 +01:00
Hassan Althaf
5facb772ea Update 9-templating.md
Change file name 'Renderable' to 'Engine' because the interface is not found by Auryn Auto Loader as it finds classes/interfaces by their file names.
2014-12-04 11:13:08 +05:30
Patrick
60e96fb592 added link to new chapter 2014-11-30 22:42:31 +01:00
Patrick
d64b6d9aa1 missing newline 2014-11-30 22:41:27 +01:00
Patrick
87983c57e1 new chapter 2014-11-30 22:38:48 +01:00
Patrick
c7a16da764 Merge branch 'master' of https://github.com/PatrickLouys/no-framework-tutorial 2014-11-30 21:06:53 +01:00
Patrick
87b4cf3a76 changed to controllers 2014-11-30 21:06:11 +01:00
Patrick Louys
8ca3cc47ab Merge pull request #10 from harikt/patch-1
changed aura/http to aura/web
2014-11-28 10:06:06 +01:00
Hari K T
89007243d7 aura/web , not aura/http . 2014-11-27 22:38:41 +05:30
Patrick
d51b44c6d9 this fixes #9 2014-11-16 20:04:42 +01:00
Patrick
9710403de1 missing spaces 2014-11-07 09:37:17 +01:00
Patrick
27b3e57eb9 finished templating chapter 2014-11-07 00:17:34 +01:00
Patrick
a9ab29ea93 intro adapter 2014-11-06 23:17:13 +01:00
Patrick
adba44c3c0 removed faulty code 2014-11-06 22:34:31 +01:00
Patrick
e2f077f8d9 added link to alternative opinion 2014-11-06 20:47:42 +01:00
Patrick
d719961831 fixed typo 2014-11-06 20:47:22 +01:00
Patrick Louys
7173cbb6b6 Merge pull request #5 from DaveRandom/absolute-include-paths
Always use absolute paths for includes
2014-11-06 12:35:54 +01:00
Chris Wright
e2ca3a39ef Always use absolute paths for includes
It's not safe to assume that the web server will always give you a sane
cwd.
2014-11-06 11:33:04 +00:00
Patrick Louys
b4ecd15790 Update README.md 2014-10-11 18:12:58 +02:00
Patrick
159c7d78ab improved writing 2014-10-10 16:05:22 +02:00
Patrick
8f42bb59ad added missing sentence 2014-10-09 19:18:45 +02:00
Patrick
169b846419 added to intro 2014-10-08 19:08:26 +02:00
Patrick
97c249f01f fixed links 2014-10-07 23:05:55 +02:00
Patrick
21c838d2c9 fixed filename 2014-10-07 23:05:08 +02:00
Patrick
f54f2092aa into templating 2014-10-07 22:59:11 +02:00
Patrick
bfc7e12a45 added next topic 2014-10-06 22:48:28 +02:00
Patrick Louys
e27b5ef283 Update README.md 2014-10-05 21:03:11 +02:00
Patrick
7e07790e76 refactored to match earlier changes in different part 2014-09-27 16:58:34 +02:00
Patrick
ece781990d Changed former controller part 2014-09-20 19:56:51 +02:00
Patrick
26e04d9c0a Changed former controller part 2014-09-20 19:56:08 +02:00
Patrick
31b293c49b changed di recommendation 2014-09-19 15:50:19 +02:00
Patrick
2b12237f12 finished DI part 2014-09-18 21:53:50 +02:00
Patrick
4759982c32 changed to correct version number 2014-09-17 22:45:17 +02:00
Patrick
0cf4e70cae switched from uri to path 2014-09-17 22:14:25 +02:00
Patrick
38f6886933 added code formatting 2014-09-17 22:07:26 +02:00
Patrick
13a5d0fb01 added code formatting 2014-09-17 22:06:16 +02:00
Patrick
479b5d3ff8 expanded di part 2014-09-17 22:05:09 +02:00
Patrick
79cd1b51b9 added content to di part 2014-09-17 21:46:06 +02:00
Patrick
1919bffec9 started di part 2014-09-17 21:22:11 +02:00
Patrick
070a8b0563 removed partial sentence 2014-09-16 21:53:59 +02:00
Patrick
a710473db5 fixed link 2014-09-16 21:53:11 +02:00
Patrick
768e2ec17e fixed title 2014-09-16 21:52:08 +02:00
Patrick
a847391433 fixed the filename 2014-09-16 21:51:20 +02:00
Patrick
ae6c24da5d finished inversion of control part 2014-09-16 21:48:41 +02:00
Patrick
0427da3caa fixed navigation 2014-09-16 21:16:49 +02:00
Patrick
d54233080f prepared DI part and navigation 2014-09-16 21:14:29 +02:00
Patrick
cf835164e5 added content to the controller part 2014-09-16 21:12:54 +02:00
Patrick Louys
e61576821e Merge pull request #1 from halfer/master
Some suggested writing fixes
2014-09-16 18:11:25 +02:00
173 changed files with 16349 additions and 327 deletions

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
**/vendor/
**/.php-cs-fixer.cache
.idea/
.vagrant/
/identifier.sqlite
*.log
**/*.sqlite

77
01-front-controller.md Normal file
View file

@ -0,0 +1,77 @@
[next >>](02-composer.md)
### Front Controller
A [front controller](http://en.wikipedia.org/wiki/Front_Controller_pattern) is a single point of entry for your
application.
To start, create an empty directory for your project. You also need an entry point where all requests will go to. This
means you will have to create an `index.php` file.
A common way to do this is to just put the `index.php` in the root folder of the projects. Let me explain why you should not do this.
The `index.php` is the starting point, so it has to be inside the web server directory. This means that the web server
has access to all subdirectories. If you set things up properly, you can still prevent it from accessing your subfolders
where your application files are.
But sometimes things don't go according to plan. And if something goes wrong and your files are set up as above, your
whole application source code could be exposed to visitors. I won't have to explain why this is not a good thing.
So instead of doing that, create a folder in your project folder called `public`. This is a good time to create an `src`
folder for your application, also in the project root folder.
Inside the `public` folder you can now create your `index.php`. Remember that you don't want to expose anything here, so
put just the following code in there:
```php
<?php
declare(strict_types=1);
require __DIR__ . '/../src/Bootstrap.php';
```
`__DIR__` is a [magic constant](http://php.net/manual/en/language.constants.predefined.php) that contains the path of
the directory. By using it, you can make sure that the `require` always uses the same relative path to the file it is
used in. Otherwise, if you call the `index.php` from a different folder it will not find the file.
`declare(strict_types=1);` sets the current file
to [strict typing](http://php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration.strict). In
this tutorial we are going to use this for all PHP files. This means that you can't just pass an integer as a parameter
to a method that requires a string. If you don't use strict mode, it would be automatically cast to the required type.
With strict mode, it will throw an Exception if it is the wrong type. I would always advise you to use strict mode as
that helps you catch possible errors early.
The `Bootstrap.php` will be the file that wires your application together. We will get to it shortly.
The rest of the public folder is reserved for your public asset files (like JavaScript files and stylesheets).
Now navigate inside your `src` folder and create a new `Bootstrap.php` file with the following content:
```php
<?php
declare(strict_types=1);
echo 'Hello World!';
```
Now let's see if everything is set up correctly. Open up a console and navigate into your projects `public` folder. In
there type `php -S 0.0.0.0:1235` and press enter. This will start the built-in webserver and you can access your page in
a browser with `http://localhost:1235`. You should now see the 'hello world' message.
If there is an error, go back and try to fix it. If you only see a blank page, check the console window where the server
is running for errors.
Now would be a good time to commit your progress. If you are not already using Git, set up a repository now. This is not
a Git tutorial so I won't go over the details. But using version control should be a habit, even if it is just for a
tutorial project like this.
Some editors and IDE's put their own files into your project folders. If that is the case, create a `.gitignore` file in
your project root and exclude the files/directories. Below is an example for PHPStorm:
```
.idea/
```
[next >>](02-composer.md)

82
02-composer.md Normal file
View file

@ -0,0 +1,82 @@
[<< previous](01-front-controller.md) | [next >>](03-error-handler.md)
### Composer
[Composer](https://getcomposer.org/) is a dependency manager for PHP.
Just because you are not using a framework does not mean you will have to reinvent the wheel every time you want to do
something. With Composer, you can install third-party libraries for your application.
If you don't have Composer installed already, head over to the website and install it. You can find Composer packages
for your project on [Packagist](https://packagist.org/).
Create a new file in your project root folder called `composer.json`. This is the Composer configuration file that will
be used to configure your project and its dependencies. It must be valid JSON or Composer will fail.
Add the following content to the file:
```json
{
"name": "lubiana/no-framework",
"type": "project",
"license": "MIT",
"autoload": {
"psr-4": {
"Lubiana\\NoFramework\\": "src/"
}
},
"authors": [
{
"name": "lubiana",
"email": "lubiana@hannover.ccc.de"
}
],
"require": {
"php": "^8.4"
}
}
```
In the autoload part you can see that I am using the `Lubian\NoFramework` namespace for the project. You can use
whatever fits your project there, but from now on I will always use the `Lubian\NoFramework` namespace in my examples.
Just replace it with your namespace in your own code.
In the `require` block I specified, that the minimum PHP Version to run this code is 8.4. That helps composer to find
suitable versions of libraries we want to install later on.
I have also defined, that all my code and classes in the 'Lubian\NoFramework' namespace lives under the './src' folder.
As the Bootstrap.php file is placed in that directory we should
add the namespace to the File as well. Here is my current Bootstrap.php
as a reference:
```php
<?php
declare(strict_types=1);
namespace Lubiana\NoFramework;
echo 'Hello World!';
```
Open a new console window and navigate into your project root folder. There run `composer update`.
Composer creates a `composer.lock` file that locks in your dependencies and a vendor directory.
Committing the `composer.lock` file into version control is generally good practice for projects. It allows
continuation testing tools (such as [Travis CI](https://travis-ci.org/)) to run the tests against the exact same
versions of libraries that you're developing against. It also allows all people who are working on the project to use
the exact same version of libraries i.e. it eliminates a source of "works on my machine" problems.
That being said, [you don't want to put the actual source code of your dependencies in your git repository](https://getcomposer.org/doc/faqs/should-i-commit-the-dependencies-in-my-vendor-directory.md). So let's add a rule to our `.gitignore` file:
```
vendor/
```
Now you have successfully created an empty playground which you can use to set up your project.
[<< previous](01-front-controller.md) | [next >>](03-error-handler.md)

133
03-error-handler.md Normal file
View file

@ -0,0 +1,133 @@
[<< previous](02-composer.md) | [next >>](04-development-helpers.md)
### Error Handler
An error handler allows you to customize what happens if your code results in an error.
It is important to clearly define how your project should behave when it encounters an error, and especially where it
should output the information about an error.
We want to mainly focus on Exceptions, which are an encapsulation of errors in php. We also have "normal" errors, but
most of them are currently being changed currently into Exceptions.
```php
<?php
declare(strict_types=1);
namespace Lubiana\NoFramework;
use ErrorException;
use Throwable;
set_exception_handler(function (Throwable $t) {
$errorType = match($t->getCode()) {
E_ERROR, E_USER_ERROR => 'Fatal Error',
E_WARNING, E_USER_WARNING => 'Warning',
E_NOTICE, E_USER_NOTICE => 'Notice',
default => 'Unknown Error'
};
echo <<<HTML
<h1>{$errorType}</h1>
<p>{$t->getMessage()}</p>
<pre>{$t->getTraceAsString()}</pre>
HTML;
});
set_error_handler(
function (int $errno, string $errstr, string $errfile, int $errline) {
throw new ErrorException(
message: $errstr,
code: $errno,
severity: $errno,
filename: $errfile,
line: $errline
);
}
);
echo 'Hello world!';
```
You can then replace `echo 'Hello world!';` with `trigger_error('This is a test error');`
or `throw new Exception('This is a test exception');` and open it in your browser to see if the error handling works.
During development there are some other nice features to add. For example a quick link to open your Editor on the file the Error occured. So the first package
for your application will take care of that.
I like [filp/whoops](https://github.com/filp/whoops), so I will show how you can install that package for your project.
If you prefer another package, feel free to install that one. This is the beauty of programming without a framework,
you have total control over your project.
Some alternatives would be: [PHP-Error](https://github.com/JosephLenton/PHP-Error) or [Tracy](https://tracy.nette.org/en/)
To install that package into your project simply type `composer require filp/whoops` into your terminal at the project root,
now composer automatically looks for a version of that package compatible with the rest of your project and your php
version.
But you can't use it yet. PHP won't know where to find the files for the classes. For this you will need an autoloader,
ideally a [PSR-4](http://www.php-fig.org/psr/psr-4/) autoloader. Composer already takes care of this for you, so you
only have to add a `require __DIR__ . '/../vendor/autoload.php';` to your `Bootstrap.php`.
**Important:** Never show any errors in your production environment. A stack trace or even just a simple error message
can help someone to gain access to your system. Always show a user-friendly error page instead and send an email to
yourself, write to a log or something similar. So only you can see the errors in the production environment.
For development that does not make sense, though -- you want a nice error page. The solution is to have an environment
switch in your code. We use the getenv() function here to check the environment and define the 'dev' env as standard in
case no environment has been set.
Then after the error handler registration, throw an `Exception` to test if everything is working correctly.
Your `Bootstrap.php` should now look similar to this:
```php
<?php
declare(strict_types=1);
namespace Lubiana\NoFramework;
use Throwable;
use Whoops\Handler\CallbackHandler;
use Whoops\Handler\PrettyPageHandler;
use Whoops\Run;
require __DIR__ . '/../vendor/autoload.php';
$environment = getenv('APP_ENV') ?: 'dev';
$whoops = new Run;
$whoops->pushHandler(
new CallbackHandler(
function (Throwable $e) use ($environment) {
if ($environment !== 'dev') {
http_response_code(500);
echo 'Whoops';
}
error_log(<<<TXT
Error: {$e->getMessage()}
{$e->getTraceAsString()}
TXT
);
}
)
);
if ($environment === 'dev') {
$whoops->pushHandler(new PrettyPageHandler);
}
$whoops->register();
throw new \Exception('Hello world');
```
You should now see a error page with the line highlighted where you throw the exception. If not, go back and debug until
you get it working. Now would also be a good time for another commit.
**Side-note:** Here we use `getenv()` to read a Variable from the Environment and fallback to using 'dev' if it is not set. That is a bad default, and in a production app you should always default to the strictest mode.
There are also good libraries that help with managing that in a better fashion. You can take a look at [phpdotenv](https://github.com/vlucas/phpdotenv) or [symfony/dotenv](https://github.com/symfony/dotenv) and maybe implement them. This tutorial however skips this step.
[<< previous](02-composer.md) | [next >>](04-development-helpers.md)

303
04-development-helpers.md Normal file
View file

@ -0,0 +1,303 @@
[<< previous](03-error-handler.md) | [next >>](05-http.md)
### Development Helpers
I have added some more helpers to my composer.json that help me with development. As these are scripts and programms
used only for development they should not be used in a production environment. Composer has a specific sections in its
file called "dev-dependencies", everything that is required in this section does not get installed in production.
Let's install our dev-helpers and i will explain them one by one:
`composer require --dev phpstan/phpstan symfony/var-dumper slevomat/coding-standard symplify/easy-coding-standard rector/rector`
#### Static Code Analysis with phpstan
Phpstan is a great little tool, that tries to understand your code and checks if you are making any grave mistakes or
create bad defined interfaces and structures. It also helps in finding logic-errors, dead code, access to array elements
that are not (or not always) available, if-statements that always are true and a lot of other stuff.
A very simple example would be a small functions that takes a DateTime-Object and prints it in a human-readable format.
```php
/**
* @param \DateTime $date
* @return void
*/
function printDate($date) {
$date->format('Y-m-d H:i:s');
}
printDate('now');
```
if we run phpstan with the command `./vendor/bin/phpstan analyse --level 9 ./src/`
It firstly tells us that calling "format" on a DateTime-Object without outputting or returning the function result has
no use, and secondly, that we are calling the function with a string instead of a datetime object.
```shell
1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
------ ---------------------------------------------------------------------------------------------
Line Bootstrap.php
------ ---------------------------------------------------------------------------------------------
30 Call to method DateTime::format() on a separate line has no effect.
33 Parameter #1 $date of function Lubian\NoFramework\printDate expects DateTime, string given.
------ ---------------------------------------------------------------------------------------------
```
The second error is something that "declare strict-types" already catches for us, but the first error is something that
we usually would not discover easily without specially looking for this error-type.
We can add a simple config file called `phpstan.neon` to our project so that we do not have to specify the error level and
path everytime we want to check our code for errors:
```yaml
parameters:
level: max
paths:
- src
```
now we can just call `./vendor/bin/phpstan analyze` and have the same setting for every developer working in our project
With this settings we have already a great setup to catch some errors before we execute the code, but it still allows us
some silly things, therefore we want to add install some packages that enforce rules that are a little stricter.
```shell
composer require --dev phpstan/extension-installer
composer require --dev phpstan/phpstan-strict-rules thecodingmachine/phpstan-strict-rules
```
During the first install you need to allow the extension installer to actually install the extension. The second command
installs some more strict rules and activates them in phpstan.
If we now rerun phpstan it already tells us about some errors we have made:
```
------ -----------------------------------------------------------------------------------------------
Line Bootstrap.php
------ -----------------------------------------------------------------------------------------------
10 Short ternary operator is not allowed. Use null coalesce operator if applicable or consider
using long ternary.
25 Do not throw the \Exception base class. Instead, extend the \Exception base class. More info:
http://bit.ly/subtypeexception
26 Unreachable statement - code above always terminates.
------ -----------------------------------------------------------------------------------------------
```
The last two Errors are caused by the Exception we have used to test the ErrorHandler in the last chapter if we remove
that we should be able to fix that. The first error is something we could fix, but I don't want to focus on that specific
problem right now. Phpstan gives us the option to ignore some errors and handle them later. If for example we are working
on an old legacy codebase and wanted to add static analysis to it but can't because we would get 1 Million error messages
everytime we use phpstan, we could add all those errors to a list and tell phpstan to only bother us about new errors we
are adding to our code.
In order to use that we have to add an empty file `phpstan-baseline.neon` to our project, include that in the
`phpstan.neon` file and run phpstan with the `--generate-baseline` option:
```yaml
includes:
- phpstan-baseline.neon
parameters:
level: max
paths:
- src
```
```shell
[vagrant@archlinux app]$ ./vendor/bin/phpstan analyze --generate-baseline
Note: Using configuration file /home/vagrant/app/phpstan.neon.
1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
[OK] Baseline generated with 1 error.
```
you can read more about the possible parameters and usage options in the [documentation](https://phpstan.org/user-guide/getting-started)
#### Easy-Coding-Standard
There are two great tools that help us with applying a consistent coding style to our project as well as check and
automatically fix some other errors and oversights that we might not bother with when writing our code.
The first one is [PHP Coding Standards Fixer](https://cs.symfony.com/) which can automatically detect violations of
a defined coding standard and fix them. The second tool is [PHP CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer)
which basically does the same has in my experience some more Rules available that we can apply to our code.
But we are going to use neither of those tools directly and instead choose the [Easy Coding Standard](https://github.com/symplify/easy-coding-standard)
which allows us to combine rules from both mentioned tools, and also claims to run much faster. You could check out the
documentation and decide on your own coding standard. Or use the one provided by me, which is base on PSR-12 but adds
some highly opiniated options. First create a file 'ecs.php' and either add your own configuration or copy the
prepared one:
```php
<?php declare(strict_types=1);
use PhpCsFixer\Fixer\Import\OrderedImportsFixer;
use PhpCsFixer\Fixer\Operator\NewWithBracesFixer;
use PhpCsFixer\Fixer\PhpTag\BlankLineAfterOpeningTagFixer;
use SlevomatCodingStandard\Sniffs\Classes\ClassConstantVisibilitySniff;
use SlevomatCodingStandard\Sniffs\ControlStructures\NewWithoutParenthesesSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\AlphabeticallySortedUsesSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\DisallowGroupUseSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\MultipleUsesPerLineSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\NamespaceSpacingSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\ReferenceUsedNamesOnlySniff;
use SlevomatCodingStandard\Sniffs\Namespaces\UseSpacingSniff;
use SlevomatCodingStandard\Sniffs\TypeHints\DeclareStrictTypesSniff;
use SlevomatCodingStandard\Sniffs\TypeHints\UnionTypeHintFormatSniff;
use Symplify\EasyCodingStandard\Config\ECSConfig;
use Symplify\EasyCodingStandard\ValueObject\Set\SetList;
return static function (ECSConfig $config): void {
$config->parallel();
$config->paths([__DIR__ . '/src', __DIR__ . '/ecs.php', __DIR__ . '/rector.php']);
$config->skip([BlankLineAfterOpeningTagFixer::class, OrderedImportsFixer::class, NewWithBracesFixer::class]);
$config->sets([
SetList::PSR_12,
SetList::STRICT,
SetList::ARRAY,
SetList::SPACES,
SetList::DOCBLOCK,
SetList::CLEAN_CODE,
SetList::COMMON,
SetList::COMMENTS,
SetList::NAMESPACES,
SetList::SYMPLIFY,
SetList::CONTROL_STRUCTURES,
]);
// force visibility declaration on class constants
$config->ruleWithConfiguration(ClassConstantVisibilitySniff::class, [
'fixable' => true,
]);
// sort all use statements
$config->rules([
AlphabeticallySortedUsesSniff::class,
DisallowGroupUseSniff::class,
MultipleUsesPerLineSniff::class,
NamespaceSpacingSniff::class,
]);
// import all namespaces, and even php core functions and classes
$config->ruleWithConfiguration(
ReferenceUsedNamesOnlySniff::class,
[
'allowFallbackGlobalConstants' => false,
'allowFallbackGlobalFunctions' => false,
'allowFullyQualifiedGlobalClasses' => false,
'allowFullyQualifiedGlobalConstants' => false,
'allowFullyQualifiedGlobalFunctions' => false,
'allowFullyQualifiedNameForCollidingClasses' => true,
'allowFullyQualifiedNameForCollidingConstants' => true,
'allowFullyQualifiedNameForCollidingFunctions' => true,
'searchAnnotations' => true,
]
);
// define newlines between use statements
$config->ruleWithConfiguration(UseSpacingSniff::class, [
'linesCountBeforeFirstUse' => 1,
'linesCountBetweenUseTypes' => 1,
'linesCountAfterLastUse' => 1,
]);
// strict types declaration should be on same line as opening tag
$config->ruleWithConfiguration(DeclareStrictTypesSniff::class, [
'declareOnFirstLine' => true,
'spacesCountAroundEqualsSign' => 0,
]);
// disallow ?Foo typehint in favor of Foo|null
$config->ruleWithConfiguration(UnionTypeHintFormatSniff::class, [
'withSpaces' => 'no',
'shortNullable' => 'no',
'nullPosition' => 'last',
]);
// Remove useless parentheses in new statements
$config->rule(NewWithoutParenthesesSniff::class);
};
```
You can now use `./vendor/bin/ecs` to list all violations of the defined standard and `./vendor/bin/ecs --fix` to
automatically fix them.
#### Rector
The next tool helps us with automatic refactorings and upgrades to newer PHP versions.
Place a file called `rector.php` in your app directory and put in the following content:
```php
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->paths([__DIR__ . '/src', __DIR__ . '/rector.php', __DIR__ . '/ecs.php']);
$rectorConfig->importNames();
$rectorConfig->sets([
LevelSetList::UP_TO_PHP_81,
]);
};
```
This config fixes your code and replaces function call and constructs that are deprecated in modern php versions. This
includes all fixes from PHP 5.2 up to PHP 8.1. You can take a look at all the rules [here](https://github.com/rectorphp/rector/blob/main/docs/rector_rules_overview.md#php52).
To run this tool simply type `./vendor/bin/rector process` in your console. This should not to much right now, but will
be quite useful when php 8.2 or newer versions are released.
#### Symfony Var-Dumper
another great tool for some quick debugging without xdebug is the symfony var-dumper. This just gives us some small
functions.
dump(); is basically like phps var_dump() but has a better looking output that helps when looking into bigger objects
or arrays.
dd() on the other hand is a function that dumps its parameters and then exits the php-script.
you could just write dd($whoops) somewhere in your bootstrap.php to check how the output looks.
#### Composer scripts
now we have a few commands that are available on the command line. I personally do not like to type complex commands
with lots of parameters by hand all the time, so I added a few lines to my `composer.json`:
```json
"scripts": {
"serve": [
"Composer\\Config::disableProcessTimeout",
"php -S 0.0.0.0:1235 -t public"
],
"phpstan": "./vendor/bin/phpstan analyze",
"baseline": "./vendor/bin/phpstan analyze --generate-baseline",
"check": "./vendor/bin/ecs",
"fix": "./vendor/bin/ecs --fix",
"rector": "./vendor/bin/rector process"
},
```
that way I can just type "composer" followed by the command name in the root of my project. if I want to start the
php dev server I can just type "composer serve" and don't have to type in the hostname, port and target directory all the
time.
You could also configure PhpStorm to automatically run these commands in the background and highlight the violations
directly in the file you are currently editing. I personally am not a fan of this approach because it often disrupts my
flow when programming and always forces me to be absolutely strict even if I am only trying out an idea for debugging.
My workflow is to just write my code the way I currently feel and that execute the phpstan and the fix scripts before
committing and pushing the code. There is a [highly opiniated blogpost](https://tomasvotruba.com/blog/2019/06/24/do-you-use-php-codesniffer-and-php-cs-fixer-phpstorm-plugin-you-are-slow-and-expensive/)
discussing that topic further. That you can read. But in the end it boils down to what you are most comfortable with.
[<< previous](03-error-handler.md) | [next >>](05-http.md)

124
05-http.md Normal file
View file

@ -0,0 +1,124 @@
[<< previous](04-development-helpers.md) | [next >>](06-router.md)
### HTTP
PHP already has a few things built in to make working with HTTP easier. For example there are the
[superglobals](http://php.net/manual/en/language.variables.superglobals.php) that contain the request information.
These are good if you just want to get a small script up and running, something that won't be hard to maintain. However,
if you want to write clean, maintainable, [SOLID](http://en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29) code,
then you will want a class with a nice object-oriented interface that you can use in your application instead.
Fortunately for us there has been a standard developed in the PHP-Community that is adopted by several Frameworks. The
standard is called [PSR-7](https://www.php-fig.org/psr/psr-7/) and has several interfaces defined that a lot of php
projects implement. This makes it easier for us to use modules developed for other frameworks in our projects.
As this is a widely adopted standard there are already several implementations available for us to use. I will choose
the laminas/laminas-diactoros package as i am an old time fan of the laminas (previously zend) project.
Some alternatives are [slim-psr7](https://github.com/slimphp/Slim-Psr7), [Guzzle](https://github.com/guzzle/psr7) and a
[lot more](https://packagist.org/providers/psr/http-message-implementation) are available for you to choose from.
Symfony ships its own Request and Response objects that do not implement the psr-7 interfaces. Therefore, I will not use
that in this tutorial, but if you understand how the psr-7 interfaces work you should have no problem in understanding
the [symfony http-foundation](https://symfony.com/doc/current/components/http_foundation.html#request).
to install the laminas psr-packages just type `composer require laminas/laminas-diactoros` into your console and hit
enter
Now you can add the following below your error handler code in your `Bootstrap.php` (and don't forget to remove the exception):
```php
$request = \Laminas\Diactoros\ServerRequestFactory::fromGlobals();
$response = new \Laminas\Diactoros\Response;
$response->getBody()->write('Hello World! ');
$response->getBody()->write('The Uri is: ' . $request->getUri()->getPath());
```
This sets up the `Request` and `Response` objects that you can use in your other classes to get request data and send a response back to the browser.
In order to actually add content to the response you have to access the body stream object of the Response and use the
write()-Method on that object.
To actually send something back, you will also need to add the following snippet at the end of your `Bootstrap.php` file:
```php
echo $response->getBody();
```
This will send the response data to the browser. If you don't do this, nothing happens as the `Response` object only
stores data.
You can play around with the other methods of the Request object and take a look at its content with the dd() function.
```php
dd($response)
```
Something you have to keep in mind is that the Response and Request objects are Immutable which means that they cannot
be changed after creation. Whenever you want to modify a property you have to call one of the "with" functions, which
creates a copy of the request object with the changed property and returns that clone:
```php
$response = $response->withStatus(200);
$response = $response->withAddedHeader('Content-type', 'application/json');
```
If you have ever struggled with Mutation-problems in an DateTime-Object you might understand why the standard has been
defined this way.
But if you have been keeping attention you might argue that the following line should not work if the request object is
immutable.
```php
$response->getBody()->write('Hello World!');
```
The response-body implements a stream interface which is immutable for some reasons that are described in the
[meta-document](https://www.php-fig.org/psr/psr-7/meta/#why-are-streams-mutable). For me the important thing is to be
aware of the problems that can occur with mutable objects. Here is a small [Blogpost](http://andrew.carterlunn.co.uk/programming/2016/05/22/psr-7-is-not-immutable.html) that gives some context. Beware that the Middleware-Example in
the post is based on a deprecated middleware standard. But more on middlewares will be discussed in later chapters.
I, for one, am happy about that fact, as it saves me from writing at least 3 lines of code whenever i want to add content
to a response object.
```php
$body = $response->getBody();
$body->write('Hello World!');
$response = $response->withBody($body);
```
Right now we are just outputting the Response-Body without any headers or http-status. So we need to expand our
output-logic a little more. Replace the line that echos the response-body with the following:
```php
foreach ($response->getHeaders() as $name => $values) {
$first = strtolower($name) !== 'set-cookie';
foreach ($values as $value) {
$header = sprintf('%s: %s', $name, $value);
header($header, $first);
$first = false;
}
}
$statusLine = sprintf(
'HTTP/%s %s %s',
$response->getProtocolVersion(),
$response->getStatusCode(),
$response->getReasonPhrase()
);
header($statusLine, true, $response->getStatusCode());
echo $response->getBody();
```
This code is still fairly simple and there is a lot more stuff that can be considered when emitting a response to a
browser, if you want a more complete solution you can take a look at the [httpsoft/http-emitter](https://github.com/httpsoft/http-emitter/blob/master/src/SapiEmitter.php) package on github.
Remember that the object is only storing data, so if you set multiple status codes before you send the response, only the last one will be applied.
Be sure to run composer phpstan, composer fix and composer check before moving on to the next chapter
[<< previous](04-development-helpers.md) | [next >>](06-router.md)

101
06-router.md Normal file
View file

@ -0,0 +1,101 @@
[<< previous](05-http.md) | [next >>](07-dispatching-to-a-class.md)
### Router
A router dispatches to different handlers depending on rules that you have set up.
With your current setup it does not matter what URL is used to access the application, it will always result in the same
response. So let's fix that now.
I will use [nikic/fast-route](https://github.com/nikic/FastRoute) in this tutorial. But as always, you can pick your own
favorite package.
Alternative packages: [symfony/Routing](https://github.com/symfony/Routing), [Aura.Router](https://github.com/auraphp/Aura.Router), [fuelphp/routing](https://github.com/fuelphp/routing), [Klein](https://github.com/chriso/klein.php)
By now you know how to install Composer packages, so I will leave that to you.
Now add this code block to your `Bootstrap.php` file where you added the 'hello world' message in the last chapter.
```php
$dispatcher = \FastRoute\simpleDispatcher(function (\FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/hello[/{name}]', function (\Psr\Http\Message\ServerRequestInterface $request) {
$name = $request->getAttribute('name', 'Stranger');
$response = (new \Laminas\Diactoros\Response)->withStatus(200);
$response->getBody()->write('Hello ' . $name . '!');
return $response;
});
$r->addRoute('GET', '/another-route', function (\Psr\Http\Message\ServerRequestInterface $request) {
$response = (new \Laminas\Diactoros\Response)->withStatus(200);
$response->getBody()->write('This works too!');
return $response;
});
});
$routeInfo = $dispatcher->dispatch(
$request->getMethod(),
$request->getUri()->getPath(),
);
switch ($routeInfo[0]) {
case \FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
$response = (new \Laminas\Diactoros\Response)->withStatus(405);
$response->getBody()->write('Method not allowed');
$response = $response->withStatus(405);
break;
case \FastRoute\Dispatcher::FOUND:
$handler = $routeInfo[1];
foreach ($routeInfo[2] as $attributeName => $attributeValue) {
$request = $request->withAttribute($attributeName, $attributeValue);
}
/** @var \Psr\Http\Message\ResponseInterface $response */
$response = call_user_func($handler, $request);
break;
case \FastRoute\Dispatcher::NOT_FOUND:
default:
$response = (new \Laminas\Diactoros\Response)->withStatus(404);
$response->getBody()->write('Not Found!');
break;
}
```
In the first part of the code, you are registering the available routes for your application. In the second part, the
dispatcher gets called and the appropriate part of the switch statement will be executed. If a route was found,
we collect any variable parameters of the route, store them in the request parameterbag and call the handler callable.
If the route dispatcher returns a wrong value in the first entry of the routeMatch array we handle it the same as a 404.
This setup might work for tiny applications, but once you start adding a few routes your bootstrap file will
quickly get cluttered. So let's move them out into a separate file.
Create a new directory in you project root named 'config' and add a 'routes.php' file with the following content;
```php
<?php declare(strict_types = 1);
return function(\FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/hello[/{name}]', function (\Psr\Http\Message\ServerRequestInterface $request) {
$name = $request->getAttribute('name', 'Stranger');
$response = (new \Laminas\Diactoros\Response)->withStatus(200);
$response->getBody()->write('Hello ' . $name . '!');
return $response;
});
$r->addRoute('GET', '/another-route', function (\Psr\Http\Message\ServerRequestInterface $request) {
$response = (new Laminas\Diactoros\Response)->withStatus(200);
$response->getBody()->write('This works too!');
return $response;
});
};
```
Now let's rewrite the route dispatcher part to use the `routes.php` file.
```php
$routeDefinitionCallback = require __DIR__ . '/../config/routes.php';
$dispatcher = \FastRoute\simpleDispatcher($routeDefinitionCallback);
```
This is already an improvement, but now all the handler code is in the `routes.php` file. This is not optimal, so let's fix that in the next part.
Of course, we now need to add the 'config' folder to the configuration files of our
dev helpers so that they can scan that directory as well.
[<< previous](05-http.md) | [next >>](07-dispatching-to-a-class.md)

View file

@ -0,0 +1,137 @@
[<< previous](06-router.md) | [next >>](08-inversion-of-control.md)
### Dispatching to a Class
In this tutorial we won't implement [MVC (Model-View-Controller)](http://martinfowler.com/eaaCatalog/modelViewController.html).
MVC can't be implemented properly in PHP anyway, at least not in the way it was originally conceived. If you want to
learn more about this, read [A Beginner's Guide To MVC](http://blog.ircmaxell.com/2014/11/a-beginners-guide-to-mvc-for-web.html)
and the followup posts.
So forget about MVC and instead let's worry about [separation of concerns](http://en.wikipedia.org/wiki/Separation_of_concerns).
We will need a descriptive name for the classes that handle the requests. For this tutorial I will use `Handler`, other
common names are 'Controllers' or 'Actions'.
Create a new folder inside the `src/` folder with the name `Action`. In this folder we will place all our action classes.
In there, create a `Hello.php` file.
```php
<?php declare(strict_types = 1);
namespace Lubian\NoFramework\Action;
final class Hello implements \Psr\Http\Server\RequestHandlerInterface
{
public function handle(\Psr\Http\Message\ServerRequestInterface $request): \Psr\Http\Message\ResponseInterface
{
$name = $request->getAttribute('name', 'Stranger');
$response = (new \Laminas\Diactoros\Response)->withStatus(200);
$response->getBody()->write('Hello ' . $name . '!');
return $response;
}
}
```
You can see that we implement the [RequestHandlerInterface](https://github.com/php-fig/http-server-handler/blob/master/src/RequestHandlerInterface.php)
that has a 'handle'-Method with requires a Request object as its parameter and returns a Response-object. For now this is
fine, but we may have to change our approach later. In any way it is good to know about this interface as we will implement
it in some other parts of our application as well. In order to use that Interface we have to require it with composer:
`composer require psr/http-server-handler`.
The autoloader will only work if the namespace of a class matches the file path and the file name equals the class name.
At the beginning I defined `Lubian\NoFramework` as the root namespace of the application so this is referring to the `src/` folder.
Now let's change the hello world route so that it calls your new class method instead of the closure. Change your `routes.php` to this:
```php
return function(\FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/hello[/{name}]', \Lubian\NoFramework\Action\Hello::class);
$r->addRoute('GET', '/another-route', \Lubian\NoFramework\Action\Another::class);
};
```
Instead of a callable we are now passing the fully namespaced class identifier to the route-definition. I also declared
the class 'Another' as the target for the second route, you can create it by copying the Hello.php file and changing
the response to the one we defined for the second route.
To make this work, you will also have to do a small refactor to the routing part of the `Bootstrap.php`:
```php
case \FastRoute\Dispatcher::FOUND:
$handler = new $routeInfo[1];
if (! $handler instanceof \Psr\Http\Server\RequestHandlerInterface) {
throw new \Exception('Invalid Requesthandler');
}
foreach ($routeInfo[2] as $attributeName => $attributeValue) {
$request = $request->withAttribute($attributeName, $attributeValue);
}
$response = $handler->handle($request);
assert($response instanceof \Psr\Http\Message\ResponseInterface)
break;
```
So instead of just calling a method you are now instantiating an object and then calling the method on it.
Now if you visit `http://localhost:1235/` everything should work. If not, go back and debug.
And of course don't forget to commit your changes.
Something that still bothers me is the fact, that we do have classes for our Handlers, but the Error responses are still
generated in the routing-matching section and not in special classes. Also, we have still left some cases to chance, for
example if there is an error in creating our RequestHandler class or if the call to the 'handle' function fails. We still
have our whoopsie error-handler, but I like to be more explicit in my control flow.
In order to do that we need to define some special Exceptions that we can throw and catch explicitly. Lets add a new
Folder/Namespace to our src directory called Exceptions. And define the classes NotFound, MethodNotAllowed and
InternalServerError. All three should extend phps Base Exception class.
Here is my NotFound.php for example.
```php
<?php
declare(strict_types=1);
namespace Lubian\NoFramework\Exception;
final class NotFound extends Exception{}
```
Use that example to create a MethodNotAllowedException.php and InternalServerErrorException.php as well.
After you have created those we update our Router code to use the new Exceptions.
```php
try {
switch ($routeInfo[0]) {
case Dispatcher::FOUND:
$className = $routeInfo[1];
$handler = new $className;
assert($handler instanceof RequestHandlerInterface);
foreach ($routeInfo[2] as $attributeName => $attributeValue) {
$request = $request->withAttribute($attributeName, $attributeValue);
}
$response = $handler->handle($request);
break;
case Dispatcher::METHOD_NOT_ALLOWED:
throw new MethodNotAllowed;
case Dispatcher::NOT_FOUND:
default:
throw new NotFound;
}
} catch (MethodNotAllowed) {
$response = (new Response)->withStatus(405);
$response->getBody()->write('Not Allowed');
} catch (NotFound) {
$response = (new Response)->withStatus(404);
$response->getBody()->write('Not Found');
} catch (Throwable $t) {
throw new InternalServerError($t->getMessage(), $t->getCode(), $t);
}
```
Check if our code still works, try to trigger some errors, run phpstan and the fix command
and don't forget to commit your changes.
[<< previous](06-router.md) | [next >>](08-inversion-of-control.md)

View file

@ -0,0 +1,54 @@
[<< previous](07-dispatching-to-a-class.md) | [next >>](09-dependency-injector.md)
### Inversion of Control
In the last part you have set up a controller class and generated our Http-Response-object in that class, but if we
want to switch to a more powerfull Http-Implementation later, or need to create our own for some special purposes, then
we would need to edit every one of our request handlers to call a different constructor of the class.
The sane option is to use [inversion of control](http://en.wikipedia.org/wiki/Inversion_of_control). This means that
instead of giving the class the responsibility of creating the object it needs, you just ask for them. This is done
with [dependency injection](http://en.wikipedia.org/wiki/Dependency_injection).
If this sounds a little complicated right now, don't worry. Just follow the tutorial and once you see how it is
implemented, it will make sense.
Change your `Hello` action to the following:
```php
<?php declare(strict_types = 1);
namespace Lubian\NoFramework\Action;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
final class Hello implements \Psr\Http\Server\RequestHandlerInterface;
{
public function __construct(
private Response $response
) {}
public function handle(Request $request): Response
{
$name = $request->getAttribute('name', 'Stranger');
$body = $this->response->getBody();
$body->write('Hello ' . $name . '!');
return $this->response
->withBody($body)
->withStatus(200);
}
}
```
Now the code will result in an error because we are not actually injecting anything. So let's fix that in the `Bootstrap.php` where we dispatch when a route was found:
```php
$handler = new $className($response);
```
Of course we need to also update all the other handlers.
[<< previous](07-dispatching-to-a-class.md) | [next >>](09-dependency-injector.md)

243
09-dependency-injector.md Normal file
View file

@ -0,0 +1,243 @@
[<< previous](08-inversion-of-control.md) | [next >>](10-invoker.md)
### Dependency Injector
In the last chapter we rewrote our Actions to require the response-objet as a constructor parameter, and provided it
in the dispatcher section of our `Bootstrap.php`. As we only have one dependency this works really fine, but if we have
different classes with different dependencies our bootstrap file gets complicated quite quickly. Let's look at an example
to explain the problem and work on a solution.
#### Adding a clock service
Lets assume that we want to show the current time in our Hello action. We could easily just call use one of the many
ways to get the current time directly in the handle-method, but let's create a separate class and interface for that so
we can later configure and switch our implementation.
We need a new 'Service\Time' namespace, so lets first create the folder in our 'src' directory 'src/Service/Time'.
There we place a Clock.php interface and a SystemClock.php implementation:
The Clock.php interface:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Service\Time;
interface Clock
{
public function now(): \DateTimeImmutable;
}
```
The Clock interface is modelled after the [proposed clock interface psr](https://github.com/php-fig/fig-standards/blob/master/proposed/clock.md)
which may or may not one day be accepted as an official standard.
The SystemClock.php implementation:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Service\Time;
final class SystemClock implements Clock
{
public function now(): \DateTimeImmutable
{
return new \DateTimeImmutable();
}
}
```
Now we can require the Clock interface as a dependency in our controller and use it to display the current time.
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Action;
use Lubian\NoFramework\Service\Time\Clock;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class Hello implements RequestHandlerInterface
{
public function __construct(
private readonly ResponseInterface $response,
private readonly Clock $clock
)
{
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
$name = $request->getAttribute('name', 'Stranger');
$body = $this->response->getBody();
$time = $this->clock->now()->format('H:i:s');
$body->write('Hello ' . $name . '!<br />');
$body->write('The Time is: ' . $time);
return $this->response->withBody($body)
->withStatus(200);
}
}
```
But if we try to access the corresponding route in the browser we get an error:
> Too few arguments to function Lubian\NoFramework\Action\Hello::__construct(), 1 passed in /home/lubiana/PhpstormProjects/no-framework/app/src/Bootstrap.php on line 62 and exactly 2 expected
Our current problem is, that we have two Actions defined, which both have different constructor requirements. That means,
that we need to have some code in our Application, that creates our Action Objects and takes care of injection all the
needed dependencies.
This code is called a Dependency Injector. If you want you can read [this](https://afilina.com/learn/design/dependency-injection)
great blogpost about that topic, which I highly recommend.
Lets build our own Dependency Injector to make our application work again.
As a starting point we are going to take a look at the [Container Interface](https://www.php-fig.org/psr/psr-11/) that
is widely adopted in the PHP-World.
#### Building a dependency container
**Short Disclaimer:** *Although it would be fun to write our own great implementation of this interface with everything that
is needed for modern php development I will take a shortcut here and implement very reduced version to show you the
basic concept.*
The `Psr\Container\ContainerIterface` defines two methods:
* has($id): bool
returns true if the container can provide a value for a given ID
* get($id): mixed
returns some kind of value that is registered in the container for the given ID
I mostly define an Interface or a fully qualified classname as an ID. That way I can query the container for
the Clock interface or an Action class and get an object of that class or an object implementing the given Interface.
For the sake of this tutorial we will put a new file in our config folder that returns an anonymous class implementing
the container-interface.
In this class we will configure all services required for our application and make them accessible via the get($id)
method.
Before we can implement the interface we need to install its definition with composer `composer require "psr/container:^1.0"`.
now we can create a file with a Class that implements that interface.
`config/container.php`:
```php
<?php declare(strict_types=1);
return new class () implements \Psr\Container\ContainerInterface {
private readonly array $services;
public function __construct()
{
$this->services = [
\Psr\Http\Message\ServerRequestInterface::class => fn () => \Laminas\Diactoros\ServerRequestFactory::fromGlobals(),
\Psr\Http\Message\ResponseInterface::class => fn () => new \Laminas\Diactoros\Response(),
\FastRoute\Dispatcher::class => fn () => \FastRoute\simpleDispatcher(require __DIR__ . '/routes.php'),
\Lubian\NoFramework\Service\Time\Clock::class => fn () => new \Lubian\NoFramework\Service\Time\SystemClock(),
\Lubian\NoFramework\Action\Hello::class => fn () => new \Lubian\NoFramework\Action\Hello(
$this->get(\Psr\Http\Message\ResponseInterface::class),
$this->get(\Lubian\NoFramework\Service\Time\Clock::class)
),
\Lubian\NoFramework\Action\Other::class => fn () => new \Lubian\NoFramework\Action\Other(
$this->get(\Psr\Http\Message\ResponseInterface::class)
),
];
}
public function get(string $id)
{
if (! $this->has($id)) {
throw new class () extends \Exception implements \Psr\Container\NotFoundExceptionInterface {
};
}
return $this->services[$id]();
}
public function has(string $id): bool
{
return array_key_exists($id, $this->services);
}
};
```
Here I have declared a services array, that has a class- or interface name as the keys, and the values are short
closures that return an Object of the defined class or interface. The `has` method simply checks if the given id is
defined in our services array, and the `get` method calls the closure defined in the array for the given id key and then
returns the result of that closure.
To use the container we need to update our Bootstrap.php. Firstly we need to get an instance of our container, and then
use that to create our Request-Object as well as the Dispatcher. So remove the manual instantiation of those objects and
replace that with the following code:
```php
$container = require __DIR__ . '/../config/container.php';
assert($container instanceof \Psr\Container\ContainerInterface);
$request = $container->get(\Psr\Http\Message\ServerRequestInterface::class);
assert($request instanceof \Psr\Http\Message\ServerRequestInterface);
$dispatcher = $container->get(FastRoute\Dispatcher::class);
assert($dispatcher instanceof \FastRoute\Dispatcher);
```
In the Dispatcher switch block we manually build our handler object with this two lines:
```php
$handler = new $className($response);
assert($handler instanceof RequestHandlerInterface);
```
Instead of manually creating the Handler-Instance we are going to kindly ask the Container to build it for us:
```php
$handler = $container->get($className);
assert($handler instanceof RequestHandlerInterface);
```
If you now open the `/hello` route in your browser everything should work again!
#### Using Auto wiring
If you take a critical look at the services array you might see that we need to manually define how our Hello- and
Other-Action are getting constructed. This is quite repetitive, as we have already declared what objects to create
when asking for the ResponseInterface and the Clock-Interface. We would need to write way less code, if our Container
was smart enough to automatically figure our which services to Inject by looking at the constructor of a class.
PHP provides us with the great Reflection Api that is capable of showing us, [what arguments a constructor of any
given class requires](https://www.php.net/manual/de/reflectionclass.getconstructor.php). We could implement that
functionality ourselves, or just try to use a library that takes care of that for us.
You can query the composer database to find all [libraries that implement the container interface](https://packagist.org/providers/psr/container-implementation).
I choose the [PHP-DI](https://packagist.org/packages/php-di/php-di) container, as it is easy to configure and provides some very [powerfull features](https://php-di.org/#autowiring)
out of the box, and also solves the auto wiring problem.
Let's rewrite our `container.php` file to use the PHP-DI container and only define the Services the Container cannot
automatically build.
```php
<?php declare(strict_types=1);
$builder = new \DI\ContainerBuilder;
$builder->addDefinitions([
\Psr\Http\Message\ServerRequestInterface::class => fn () => \Laminas\Diactoros\ServerRequestFactory::fromGlobals(),
\Psr\Http\Message\ResponseInterface::class => fn () => new \Laminas\Diactoros\Response(),
\FastRoute\Dispatcher::class => fn () => \FastRoute\simpleDispatcher(require __DIR__ . '/routes.php'),
\Lubian\NoFramework\Service\Time\Clock::class => fn () => new \Lubian\NoFramework\Service\Time\SystemClock(),
]);
return $builder->build();
```
As the PHP-DI container that is return by the `$builder->build()` method implements the same container interface as our
previously used ad-hoc container we won't need to update the Bootstrap file and everything still works.
[<< previous](08-inversion-of-control.md) | [next >>](10-invoker.md)

View file

@ -1,43 +0,0 @@
[next >>](2-composer.md)
### Front Controller
A [front controller](http://en.wikipedia.org/wiki/Front_Controller_pattern) is a single point of entry for your application.
To start, create an empty directory for your project. You also need an entry point where all requests will go to. This means you will have to create an `index.php` file.
A common way to do this is to just put the `index.php` in the root folder of the projects. This is also how some frameworks do it. Let me explain why you should not do this.
The `index.php` is the starting point, so it has to be inside the web server directory. This means that the web server has access to all subdirectories. If you set things up properly, you can still prevent it from accessing your subfolders where your application files are.
But sometimes things don't go according to plan. And if something goes wrong and your files are set up as above, your whole application source code could be exposed to visitors. I won't have to explain why this is not a good thing.
So instead of doing that, create a folder in your project folder called `public`. This is a good time to create an `src` folder for your application, also in the project root folder.
Inside the `public` folder you can now create your `index.php`. Remember that you don't want to expose anything here, so put just the following code in there:
```
<?php
require '../src/Bootstrap.php';
```
The `Bootstrap.php` will be the file that wires your application together. We will get to it shortly.
The rest of the public folder is reserved for your public asset files (like JavaScript files and stylesheets).
Now navigate inside your `src` folder and create a new `Bootstrap.php` file with the following content:
```
<?php
echo 'Hello World!';
```
Now let's see if everything is set up correctly. Open up a console and navigate into your projects `public` folder. In there type `php -S localhost:8000` and press enter. This will start the built-in webserver and you can access your page in a browser with `http://localhost:8000`. You should now see the 'hello world' message.
If there is an error, go back and try to fix it. If you only see a blank page, check the console window where the server is running for errors.
Now would be a good time to commit your progress. If you are not already using Git, set up a repository now. This is not a Git tutorial so I won't go over the details. But using version control should be a habit, even if it is just for a tutorial project like this.
[next >>](2-composer.md)

101
10-invoker.md Normal file
View file

@ -0,0 +1,101 @@
[<< previous](09-dependency-injector.md) | [next >>](11-templating.md)
### Invoker
Currently, all our Actions need to implement the RequestHandlerInterface, which forces us to accept the Request as the
one and only argument to our handle function, but most of the time we only need a few attributes in our Action a long
with some services and not the whole Request object with all its various properties.
If we take our Hello action for example we only need a response object, the clock service and the 'name' information from
the request-uri. And as that class only provides one simple method we could easily make that invokable as we already named
the class hello, and it would be redundant to also call the method hello. So an updated version of that class could
look like this:
```php
final class Hello
{
public function __invoke(
ResponseInterface $response,
Clock $clock,
string $name = 'Stranger'
): ResponseInterface
{
$body = $response->getBody();
$body->write('Hello ' . $name . '!<br />');
$body->write('The time is: ' . $clock->now()->format('H:i:s'));
return $response->withBody($body)
->withStatus(200);
}
}
```
It would also be neat if we could define a classname plus a method as target handler in our routes, or even a short
closure function if we want to redirect all requests from '/' to '/hello' because we have not defined a handler for the
root path of our application yet.
```php
$r->addRoute('GET', '/hello[/{name}]', Hello::class);
$r->addRoute('GET', '/other-route', [Other::class, 'handle']);
$r->addRoute('GET', '/', fn (Response $r) => $r->withStatus(302)->withHeader('Location', '/hello'));
```
In order to support this crazy route definitions we would need to write a lot of code for actually calling the result of
the route dispatcher. If the result is a name of an invokable class we would use the container to create an instance of
that class for us and then use the [reflection api](https://www.php.net/manual/en/book.reflection.php) to figure out what
arguments the __invoke function has, try to fetch all arguments from the container and then add some more from the router
if they are needed and available. The same if we have an array of a class name with a function to call, and for a simple
callable we would need to manually use reflection as well to resolve all the arguments.
But we are quite lucky as the PHP-DI container provides us with a [great 'call' method](https://php-di.org/doc/container.html#call)
which handles all of that for us.
After you added the described changes to your routes file you can modify the Dispatcher::FOUND case of you $routeInfo
switch section in the Bootstrap.php file to use the container->call() method:
```php
$handler = $routeInfo[1];
$args = $routeInfo[2];
foreach ($routeInfo[2] as $attributeName => $attributeValue) {
$request = $request->withAttribute($attributeName, $attributeValue);
}
$args['request'] = $request;
$response = $container->call($handler, $args);
```
Try to open [localhost:1235/](http://localhost:1235/) in your browser and check if you are getting redirected to '/hello'.
But by now you should know that I do not like to depend on specific implementations and the call method is not defined in
the psr/container interface. Therefore, we would not be able to use that if we are ever switching to the symfony container
or any other implementation.
Fortunately for us (or me) the PHP-CI container ships that function as its own class that is independent of the specific
container implementation, so we could use it with any container that implements the ContainerInterface. And best of all
the class ships with its own [Interface](https://github.com/PHP-DI/Invoker/blob/master/src/InvokerInterface.php) that
we could implement if we ever want to write our own implementation, or we could write an adapter that uses a different
class that solves the same problem.
But for now we are using the solution provided by PHP-DI.
So lets request a Service implementing the InvokerInterface from the container and use that inside of the switch-case block
```php
$handler = $routeInfo[1];
$args = $routeInfo[2] ?? [];
foreach ($routeInfo[2] as $attributeName => $attributeValue) {
$request = $request->withAttribute($attributeName, $attributeValue);
}
$args['request'] = $request;
$invoker = $container->get(InvokerInterface::class);
assert($invoker instanceof InvokerInterface);
$response = $invoker->call($handler, $args);
assert($response instanceof ResponseInterface);
```
Now we are able to define absolutely everything in routes that is considered a [callable](https://www.php.net/manual/de/language.types.callable.php)
by php, and even some more.
But let us move on to something more fun and add some templating functionality to our application as we are trying to build
a website in the end.
[<< previous](09-dependency-injector.md) | [next >>](11-templating.md)

159
11-templating.md Normal file
View file

@ -0,0 +1,159 @@
[<< previous](10-invoker.md) | [next >>](12-configuration.md)
### Templating
A template engine is not necessary with PHP because the language itself can take care of that. But it can make things
like escaping values easier. They also make it easier to draw a clear line between your application logic and the
template files which should only put your variables into the HTML code.
A good quick read on this is [ircmaxell on templating](http://blog.ircmaxell.com/2012/12/on-templating.html). Please
also read [this](http://chadminick.com/articles/simple-php-template-engine.html) for a different opinion on the topic.
Personally I don't have a strong opinion on the topic, so decide yourself which approach works better for you.
For this tutorial we will use a PHP implementation of [Mustache](https://github.com/bobthecow/mustache.php). So install
that package before you continue (`composer require mustache/mustache`).
Another well known alternative would be [Twig](http://twig.sensiolabs.org/).
Now please go and have a look at the source code of the
[engine class](https://github.com/bobthecow/mustache.php/blob/master/src/Mustache/Engine.php). As you can see, the class
does not implement an interface.
You could just type hint against the concrete class. But the problem with this approach is that you create tight
coupling.
In other words, all your code that uses the engine will be coupled to this mustache package. If you want to change the
implementation you have a problem. Maybe you want to switch to Twig, maybe you want to write your own class or you want
to add functionality to the engine. You can't do that without going back and changing all your code that is tightly
coupled.
What we want is loose coupling. We will type hint against an interface and not a class/implementation. So if you need
another implementation, you just implement that interface in your new class and inject the new class instead.
Instead of editing the code of the package we will use the [adapter pattern](http://en.wikipedia.org/wiki/Adapter_pattern).
This sounds a lot more complicated than it is, so just follow along.
First let's define the interface that we want. Remember the [interface segregation principle](http://en.wikipedia.org/wiki/Interface_segregation_principle).
This means that instead of large interfaces with a lot of methods we want to make each interface as small as possible.
A class can implement multiple interfaces if necessary.
So what does our template engine actually need to do? For now we really just need a simple `render` method. Create a
new folder in your `src/` folder with the name `Template` where you can put all the template related things.
In there create a new interface `Renderer.php` that looks like this:
```php
<?php declare(strict_types = 1);
namespace Lubian\NoFramework\Template;
interface Renderer
{
/** @param array<string, mixed> $data */
public function render(string $template, array $data = []): string;
}
```
Now that this is sorted out, let's create the implementation for mustache. In the same folder, create the file
`MustacheRenderer.php` with the following content:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Template;
final class MustacheRenderer implements Renderer
{
public function __construct(private \Mustache_Engine $engine){}
public function render(string $template, array $data = []): string
{
return $this->engine->render($template, $data);
}
}
```
As you can see the adapter is really simple. While the original class had a lot of methods, our adapter is really simple
and only fulfills the interface.
Of course we also have to add a definition in our `dependencies.php` file because otherwise the container won't know
which implementation he has to inject when you hint for the interface. Add this line:
```php
[
...
\Lubian\NoFramework\Template\Renderer::class => DI\create(\Lubian\NoFramework\Template\MustacheRenderer::class)
->constructor(new Mustache_Engine),
]
```
Now update the Hello.php class to require an implementation of our renderer interface
and use that to render a string using mustache syntax.
```php
final class Hello
{
public function __invoke(
ResponseInterface $response,
Now $now,
Renderer $renderer,
string $name = 'Stranger',
): ResponseInterface {
$body = $response->getBody();
$data = [
'now' => $now()->format('H:i:s'),
'name' => $name,
];
$content = $renderer->render(
'Hello {{name}}, the time is {{now}}!',
$data,
);
$body->write($content);
return $response
->withStatus(200)
->withBody($body);
}
}
```
Now go check quickly in your browser if everything works. By default Mustache uses a simple string handler.
But what we want is template files, so let's go back and change that.
To make this change we need to pass an options array to the `Mustache_Engine` constructor. So let's go back to the
`dependencies.php` file and add the following code:
```php
[
...
Mustache_Loader_FilesystemLoader::class => fn() => new Mustache_Loader_FilesystemLoader(__DIR__ . '/../templates', ['extension' => '.html']),
Mustache_Engine::class => fn (Mustache_Loader_FilesystemLoader $MLFsl) => new Mustache_Engine(['loader' => $MLFsl]),
]
```
We are passing an options array because we want to use the `.html` extension instead of the default `.mustache` extension.
Why? Other template languages use a similar syntax and if we ever decide to change to something else then we won't have
to rename all the template files.
To let PHP-DI use its magic for creating our MustacheRenderer class we need to tell it exactly how to wire all the
dependencies, therefore I defined how to create the Filesystemloader, on the next line we typehinted that loader
in the short closure which acts as a factory method for the Mustache_Engine, as PHP-DI automatically injects the Object
we can then use it in the factory.
In your project root folder, create a `templates` folder. In there, create a file `hello.html`. The content of the file should look like this:
```
<h1>Hello {{name}}</h1>
<p>The time is: {{time}}</p>
```
Now you can go back to your `Hello` action and change the render line to `$html = $this->renderer->render('hello', $data);`
Navigate to the hello page in your browser to make sure everything works.
Before you move on to the next chapter be sure to run our quality tools and commit your changes.
[<< previous](10-invoker.md) | [next >>](12-configuration.md)

102
12-configuration.md Normal file
View file

@ -0,0 +1,102 @@
[<< previous](11-templating.md) | [next >>](13-refactoring.md)
### Configuration
In the last chapter we added some more definitions to our dependencies.php in that definitions
we needed to pass quite a few configuration settings and filesystem strings to the constructors
of the classes. This might work for a small projects, but if we are growing we want to source that out to a more
explicit file that holds all the configuration values for our project.
As this is not a problem unique to our project there are already a some options available. Some projects use
[.env](https://github.com/vlucas/phpdotenv) files, others use
[.ini](https://www.php.net/manual/de/function.parse-ini-file.php), there is
[yaml](https://www.php.net/manual/de/function.yaml-parse-file.php) as well some frameworks have implemented complex
Readers for many configuration file formats that can be used, take a look at the
[laminas config component](https://docs.laminas.dev/laminas-config/reader/) for example.
As I am a big fan of writing everything in php, which gives our IDE the chance to autocomplete our code better I am
quite happy that PHP8 gives us some tools to achieve easy to use configuration via php. You can take a look at
[this blogpost](https://stitcher.io/blog/what-about-config-builders) to read about some considerations on that topic
before moving on.
For the purpose of this Tutorial I will use a simple ValueObject that has all our configuration values as properties.
create a `Configuration.php` class in the `./src` folder:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework;
final class Configuration
{
public function __construct(
public readonly string $environment = 'dev',
public readonly string $routesFile = __DIR__ . '/../config/routes.php',
public readonly string $templateDir = __DIR__ . '/../templates',
public readonly string $templateExtension = '.html',
) {
}
}
```
I am using a new Feature from PHP 8.1 here called
[readonly properties](https://stitcher.io/blog/php-81-readonly-properties) to write a small valueobject without the need
to write complex getters and setters. The linked article gives a great explanation on how they work. You can see, that
I have added working default values for every configuration parameter. In my personal opinion, project should always
have working default values without you needing to set up anything. This greatly improves usability and reduces errors.
We can now update our `container.php` file to use the configuration. Currently, the Mustache_Loader, as well as the
Fastroute-Dispatcher use values that we have defined in our Configuration, lets update those definitions:
```php
Dispatcher::class => fn (Configuration $c) => simpleDispatcher(require $c->routesFile),
Mustache_Loader_FilesystemLoader::class => fn (Configuration $c) => new Mustache_Loader_FilesystemLoader(
$c->templateDir,
[
'extension' => $c->templateExtension,
]
),
```
Magically this is all we need to do, as the PHP-DI container knows that all constructor parameters of our configuration
class have default values and can create the needed object on its own.
There is one small problem: If we want to change environment from `dev` to `prod` we would need to update the
configuration class in the src directory. This is something we don't want to do on every deployment. So lets add a file
in our `./config` directory called `settings.php` that returns a Configuration object.
```php
<?php declare(strict_types=1);
return new \Lubian\NoFramework\Configuration(
environment: 'prod',
);
```
here I am using a new feature called [named arguments](https://stitcher.io/blog/php-8-named-arguments). There is
a lot of discussion on the topic of named arguments as some argue it creates unclean and
unmaintainable code, but for simple value-objects I would argue that they are ok.
We now need to add a line to our container to use the `settings.php` file to create the Configuration-object:
```php
\Lubian\NoFramework\Configuration::class => fn () => require __DIR__ . '/settings.php',
```
One small oversight to fix is in the registration of our error-handler in the bootstrap-file. There we read the
environment with the getenv-method. Lets change the line:
```php
$environment = getenv('ENVIRONMENT') ?: 'dev';
```
to:
```php
$config = require __DIR__ . '/../config/settings.php';
assert($config instanceof \Lubian\NoFramework\Configuration);
$environment = $config->environment;
```
Check if everything still works, run your code quality checks and commit the changes before moving on the next chapter.
You might notice that phpstan throws an error as there is a documented violation missing. You can either regenerate the
baseline, or simply remove that line from the `phpstan-baseline.neon` file.
[<< previous](11-templating.md) | [next >>](13-refactoring.md)

373
13-refactoring.md Normal file
View file

@ -0,0 +1,373 @@
[<< previous](12-configuration.md) | [next >>](14-middleware.md)
### Refactoring
By now our Bootstrap.php file has grown quite a bit, and with the addition of our dependency container there is now no
reason not to introduce a lot of classes and interfaces for all the that are happening in the bootstrap file.
After all the bootstrap file should just set up the classes needed for the handling logic and execute them.
At the bottom of our Bootstrap.php we have our Response-Emitter Logic, lets create an Interface and a class for that.
As I am really lazy I just selected the code in PhpStorm, klicken on 'Refactor -> extract method' then selected the
method and clicked on 'Refactor -> extract class'. I choose 'BasicEmitter' for the classname, changed the method to non
static and extracted an interface.
'./src/Http/Emitter.php'
```php
<?php
namespace Lubian\NoFramework\Service\Http;
use Psr\Http\Message\ResponseInterface;
interface Emitter
{
public function emit(ResponseInterface $response, bool $withoutBody = false): void;
}
```
'./src/Http/BasicEmitter.php'
```php
<?php
declare(strict_types=1);
namespace Lubian\NoFramework\Service\Http;
use Psr\Http\Message\ResponseInterface;
final class BasicEmitter implements Emitter
{
public function emit(ResponseInterface $response, bool $withoutBody = false): void
{
/** @var string $name */
foreach ($response->getHeaders() as $name => $values) {
$first = strtolower($name) !== 'set-cookie';
foreach ($values as $value) {
$header = sprintf('%s: %s', $name, $value);
header($header, $first);
$first = false;
}
}
$statusLine = sprintf(
'HTTP/%s %s %s',
$response->getProtocolVersion(),
$response->getStatusCode(),
$response->getReasonPhrase()
);
header($statusLine, true, $response->getStatusCode());
if ($withoutBody) {
return;
}
echo $response->getBody();
}
}
```
After registering the BasicEmitter to implement the Emitter interface in the dependencies file you can use the following
code in the Bootstrap.php to emit the response:
```php
/** @var Emitter $emitter */
$emitter = $container->get(Emitter::class);
$emitter->emit($response);
```
If at some point you need a [more advanced emitter](https://github.com/httpsoft/http-emitter), you could now easily
write an adapter that implements your emitter interface and wraps that more advanced emitter
Now that we have our Emitter in a seperate class we need to take care of the big block that handles our routing and
calling the routerhandler that in the passes the request to a function and gets the response.
For this to steps to be seperated we are going to create two more classes:
1. a RouteDecorator, that finds the correct handler for the requests and adds its findings to the Request Object
2. A Requesthandler that implements the RequestHandlerInterface, gets the information for the request handler from the
requestobject, fetches the correct object from the container and calls it to create a response.
Lets create the HandlerInterface first:
```php
<?php
declare(strict_types=1);
namespace Lubian\NoFramework\Service\Http;
use Psr\Http\Server\RequestHandlerInterface;
interface RoutedRequestHandler extends RequestHandlerInterface
{
public function setRouteAttributeName(string $routeAttributeName = '__route_handler'): void;
}
```
By looking at the namespace and interfacename you should be able to figure out where to place the file and how to name
it.
We define a public function that the router can use to tell the handler which attribute name to look for in the request.
Now write an implementation that uses a container to satisfy the interface.
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Http;
use Invoker\InvokerInterface;
use Lubian\NoFramework\Exception\InternalServerError;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use function assert;
final class InvokerRoutedHandler implements RoutedRequestHandler
{
public function __construct(
private readonly InvokerInterface $invoker,
private string $routeAttributeName = '__route_handler',
) {
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
$handler = $request->getAttribute($this->routeAttributeName, false);
assert($handler !== false);
$vars = $request->getAttributes();
$vars['request'] = $request;
$response = $this->invoker->call($handler, $vars);
if (! $response instanceof ResponseInterface) {
throw new InternalServerError('Handler returned invalid response');
}
return $response;
}
public function setRouteAttributeName(string $routeAttributeName = '__route_handler'): void
{
$this->routeAttributeName = $routeAttributeName;
}
}
```
We will define our routing class to implement the MiddlewareInterface, you can install that with 'composer require psr/http-server-middleware'.
The interface requires us to implement a method called 'process' a Request as its first argument and an RequestHandler
as the second one. The return value of the method needs to be a Responseobject. We will learn more about Middlewares in
the next chapter.
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Http;
use FastRoute\Dispatcher;
use Lubian\NoFramework\Exception\InternalServerError;
use Lubian\NoFramework\Exception\MethodNotAllowed;
use Lubian\NoFramework\Exception\NotFound;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Throwable;
final class RouteMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly Dispatcher $dispatcher,
private readonly ResponseFactoryInterface $responseFactory,
private readonly string $routeAttributeName = '__route_handler',
) {
}
private function decorateRequest(
ServerRequestInterface $request,
): ServerRequestInterface {
$routeInfo = $this->dispatcher->dispatch(
$request->getMethod(),
$request->getUri()->getPath(),
);
if ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) {
throw new MethodNotAllowed;
}
if ($routeInfo[0] === Dispatcher::FOUND) {
foreach ($routeInfo[2] as $attributeName => $attributeValue) {
$request = $request->withAttribute($attributeName, $attributeValue);
}
return $request->withAttribute(
$this->routeAttributeName,
$routeInfo[1]
);
}
throw new NotFound;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
$request = $this->decorateRequest($request);
} catch (NotFound) {
$response = $this->responseFactory->createResponse(404);
$response->getBody()->write('Not Found');
return $response;
} catch (MethodNotAllowed) {
return $this->responseFactory->createResponse(405);
} catch (Throwable $t) {
throw new InternalServerError($t->getMessage(), $t->getCode(), $t);
}
if ($handler instanceof RoutedRequestHandler) {
$handler->setRouteAttributeName($this->routeAttributeName);
}
return $handler->handle($request);
}
}
```
Before we can use all the new services in our Bootstrap file we need to add the definitions to our container.
```php
[
'...',
Emitter::class => fn (BasicEmitter $e) => $e,
RoutedRequestHandler::class => fn (InvokerRoutedHandler $h) => $h,
MiddlewareInterface::class => fn (RouteMiddleware $r) => $r,
Dispatcher::class => fn (Settings $s) => simpleDispatcher(require __DIR__ . '/routes.php'),
ResponseFactoryInterface::class => fn (ResponseFactory $rf) => $rf,
],
```
And then we can update our Bootstrap.php to fetch all the services and let them handle the request.
```php
...
$routeMiddleWare = $container->get(MiddlewareInterface::class);
assert($routeMiddleWare instanceof MiddlewareInterface);
$handler = $container->get(RoutedRequestHandler::class);
assert($handler instanceof RequestHandlerInterface);
$emitter = $container->get(Emitter::class);
assert($emitter instanceof Emitter);
$request = $container->get(ServerRequestInterface::class);
assert($request instanceof ServerRequestInterface);
$response = $routeMiddleWare->process($request, $handler);
$emitter->emit($response);
```
Now we have wrapped all the important parts in our Bootstrap.php into seperate classes, but it is still quite a lot of
code and also many calls the container (and i have to write way too many docblocks to that phpstan doenst yell at me).
So we should just add another class that wraps all of our Request-Handling Classes into a clearly defined structure.
I will follow symfonys example and call this class our kernel. Before i create that class i will recap what our class
should require to function properly.
* A RequestFactory
We want our Kernel to be able to build the request itself
* An Emitter
Without an Emitter we will not be able to send the response to the client
* RouteMiddleware
To decore the request with the correct handler for the requested route
* RequestHandler
To delegate the request to the correct funtion that creates the response
As the Psr ContainerInterface leaves us to much handiwork to easily create a Serverrequest I will extend that interface
to give us easier access to a requestobject and wrap the Diactorors RequestFactory in an Adapter that satisfies our
interface:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Factory;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;
interface RequestFactory extends ServerRequestFactoryInterface
{
public function fromGlobals(): ServerRequestInterface;
}
```
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Factory;
use Laminas\Diactoros\ServerRequestFactory;
use Psr\Http\Message\ServerRequestInterface;
final class DiactorosRequestFactory implements RequestFactory
{
public function __construct(private readonly ServerRequestFactory $factory)
{
}
public function fromGlobals(): ServerRequestInterface
{
return $this->factory::fromGlobals();
}
public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
{
return $this->factory->createServerRequest($method, $uri, $serverParams);
}
}
```
For later shenanigans I will let our Kernel implement the RequestHandlerInterface, this is how my version looks now:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework;
use Lubian\NoFramework\Factory\RequestFactory;
use Lubian\NoFramework\Http\Emitter;
use Lubian\NoFramework\Http\RoutedRequestHandler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class Kernel implements RequestHandlerInterface
{
public function __construct(
private readonly RequestFactory $requestFactory,
private readonly MiddlewareInterface $routeMiddleware,
private readonly RoutedRequestHandler $handler,
private readonly Emitter $emitter,
) {
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->routeMiddleware->process($request, $this->handler);
}
public function run(): void
{
$request = $this->requestFactory->fromGlobals();
$response = $this->handle($request);
$this->emitter->emit($response);
}
}
```
We can now replace everything after the ErrorHandler in our Bootstrap.php with these few lines
```php
$app = $container->get(Kernel::class);
assert($app instanceof Kernel);
$app->run();
```
You might get some Errors here because the Container cannot resolve all the dependencies, try to fix those errors by looking
at the Whoops output and adding the needed definitions to the dependencies.php file.
And as always, don't forget to commit your changes.
[<< previous](12-configuration.md) | [next >>](14-middleware.md)

304
14-middleware.md Normal file
View file

@ -0,0 +1,304 @@
[<< previous](12-refactoring.md) | [next >>](15-adding-content.md)
### Middleware
In the last chapter we wrote our RouterClass to implement the middleware interface, and in this chapter I want to explain
a bit more about what this interface does and why it is used in many applications.
The Middlewares are basically a number of wrappers that stand between the client and your application. Each request gets
passed through all the middlewares, gets handled by our controllers and then the response gets passed back through all
the middlewars to the client/emitter. You can check out [this Blogpost](https://doeken.org/blog/middleware-pattern-in-php)
for a more in depth explanation of the middleware pattern.
So every Middleware can modify the request before it goes on to the next middleware (and finally the handler) and the
response after it gets created by our handlers.
So lets take a look at the middleware and the requesthandler interfaces
```php
interface MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface;
}
interface RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface;
}
```
The RequestHandlerInterface gets only a request and returns a response, the MiddlewareInterface gets a request and a
requesthandler and returns a response. So the logical thing for the Middleware is to use the handler to produce the
response.
But the middleware could just ignore the handler and produce a response on its own as the interface just requires us
to produce a response.
A simple example for that would be a caching middleware. The basic idea is that we want to cache all request from users
that are not logged in. This way we can save a lot of processing power in rendering the html and fetching data from the
database.
In this scenario we assume that we have an authentication middleware that checks if a user is logged in and decorates
the request with an 'isAuthenticated' attribute.
If the 'isAuthenticated' attribute is set to false, we check if we have a cached response and return that, if that
response is not already cached, than we let the handler create the response and store that in the cache for a few
seconds
```php
interface CacheInterface
{
public function get(string $key, callable $resolver, int $ttl): mixed;
}
```
The first parameter is the identifier for the cache, the second is a callable that produces the value and the last one
defines the seconds that the cache should keep the item. If the cache doesnt have an item with the given key then it uses
the callable to produce the value and stores it for the time specified in ttl.
so lets write our caching middleware:
```php
final class CachingMiddleware implements MiddlewareInterface
{
public function __construct(private CacheInterface $cache){}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if ($request->getAttribute('isAuthenticated', false) && $request->getMethod() === 'GET') {
$key = $request->getUri()->getPath();
return $this->cache->get($key, fn() => $handler->handle($request), 10);
}
return $handler->handle($request);
}
}
```
we can also modify the response after it has been created by our application, for example we could implement a gzip
middleware, or for more simple and silly example a middleware that adds a Dank Meme header to all our response so that the browser
know that our application is used to serve dank memes:
```php
final class DankMemeMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = $handler->handle($request);
return $response->withAddedHeader('Meme', 'Dank');
}
}
```
but for our application we are going to just add two external middlewares:
* [Trailing-slash](https://github.com/middlewares/trailing-slash) to remove the trailing slash from all routes.
* [whoops middleware](https://github.com/middlewares/whoops) to wrap our error handler into a nice middleware
```bash
composer require middlewares/trailing-slash
composer require middlewares/whoops
```
The whoops middleware should be the first middleware to be executed so that we catch any errors that are thrown in the
application as well as the middleware stack.
Our desired request -> response flow looks something like this:
Client
| ^
v |
Kernel
| ^
v |
Whoops Middleware
| ^
v |
TrailingSlash
| ^
v |
Routing
| ^
v |
ContainerResolver
| ^
v |
Controller/Action
As every middleware expects a RequestHandlerInterface as its second argument we need some extra code that wraps every
middleware as a RequestHandler and chains them together with the ContainerRouteDecoratedResolver as the last Handler.
```php
interface Pipeline
{
public function dispatch(ServerRequestInterface $request): ResponseInterface;
}
```
And our implementation looks something like this:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Http;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use function array_reverse;
use function assert;
use function is_string;
class ContainerPipeline implements Pipeline
{
/**
* @param array<MiddlewareInterface|class-string> $middlewares
*/
public function __construct(
private array $middlewares,
private RequestHandlerInterface $tip,
private ContainerInterface $container,
) {
}
public function dispatch(ServerRequestInterface $request): ResponseInterface
{
$this->buildStack();
return $this->tip->handle($request);
}
private function buildStack(): void
{
foreach (array_reverse($this->middlewares) as $middleware) {
$next = $this->tip;
if ($middleware instanceof MiddlewareInterface) {
$this->tip = $this->wrapMiddleware($middleware, $next);
}
if (is_string($middleware)) {
$this->tip = $this->wrapResolvedMiddleware($middleware, $next);
}
}
}
private function wrapResolvedMiddleware(string $middleware, RequestHandlerInterface $next): RequestHandlerInterface
{
return new class ($middleware, $next, $this->container) implements RequestHandlerInterface {
public function __construct(
private readonly string $middleware,
private readonly RequestHandlerInterface $handler,
private readonly ContainerInterface $container,
) {
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
$middleware = $this->container->get($this->middleware);
assert($middleware instanceof MiddlewareInterface);
return $middleware->process($request, $this->handler);
}
};
}
private function wrapMiddleware(MiddlewareInterface $middleware, RequestHandlerInterface $next): RequestHandlerInterface
{
return new class ($middleware, $next) implements RequestHandlerInterface {
public function __construct(
private readonly MiddlewareInterface $middleware,
private readonly RequestHandlerInterface $handler,
) {
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->middleware->process($request, $this->handler);
}
};
}
}
```
Here we define our constructor to require two arguments: an array of middlewares and a requesthandler as the final code
that should produce our response.
In the buildStack() method we wrap every middleware as a RequestHandler with the current tip property as the $next argument
and store that itself as the current tip.
There are of course a lot of more sophisticated ways to build a pipeline/dispatcher that you can check out at the [middlewares github](https://github.com/middlewares/awesome-psr15-middlewares#dispatcher)
Lets add a simple factory to our dependencies.php file that creates our middlewarepipeline
Lets create a simple Factory that loads an Array of Middlewares from the Config folder and uses that to build our pipeline
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Factory;
use Lubian\NoFramework\Http\ContainerPipeline;
use Lubian\NoFramework\Http\Pipeline;
use Lubian\NoFramework\Http\RoutedRequestHandler;
use Lubian\NoFramework\Settings;
use Psr\Container\ContainerInterface;
class PipelineProvider
{
public function __construct(
private Settings $settings,
private RoutedRequestHandler $tip,
private ContainerInterface $container,
) {
}
public function getPipeline(): Pipeline
{
$middlewares = require $this->settings->middlewaresFile;
return new ContainerPipeline($middlewares, $this->tip, $this->container);
}
}
```
And configure the container to use the Factory to create the Pipeline:
```php
...,
Pipeline::class => fn (PipelineProvider $p) => $p->getPipeline(),
...
```
And of course a new file called middlewares.php in our config folder:
```php
<?php declare(strict_types=1);
use Lubian\NoFramework\Http\RouteMiddleware;
use Middlewares\TrailingSlash;
use Middlewares\Whoops;
return [
Whoops::class,
TrailingSlash::class,
RouteMiddleware::class,
];
```
And we need to add the pipeline to our Kernel class. I will leave that as an exercise to you, a simple hint that i can
give you is that the handle()-method of the Kernel should look like this:
```php
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->pipeline->dispatch($request);
}
```
Lets try if you can make the kernel work with our created Pipeline implementation. For the future we could improve our
pipeline a little bit, so that it can accept a class-string of a middleware and resolves that with the help of a
dependency container, if you want you can do that as well.
**A quick note about docblocks:** You might have noticed, that I rarely add docblocks to my the code in the examples, and
when I do it seems kind of random. My philosophy is that I only add docblocks when there is no way to automatically get
the exact type from the code itself. For me docblocks only serve two purposes: help my IDE to understand what it choices
it has for code completion and to help the static analysis to better understand the code. There is a great blogpost
about the [cost and value of DocBlocks](https://localheinz.com/blog/2018/05/06/cost-and-value-of-docblocks/), although it
is written in 2018 at a time before PHP 7.4 was around everything written there is still valid today.
[<< previous](12-refactoring.md) | [next >>](15-adding-content.md)

253
15-adding-content.md Normal file
View file

@ -0,0 +1,253 @@
[<< previous](14-middleware.md) | [next >>](16-data-repository.md)
### Adding Content
By now we did not really display anything but some examples to in our application, and it is now time to make our app
display some content. For example we could our app be able to display the Markdown files used in this tutorial as
nicely rendered HTML Pages that can be viewed in the browser instead of the editor you are using.
So lets start by copying the markdown files to our app directory. I have created a new folder 'data/pages' and placed all
the markdown files in there.
Next we need a markdown parser, a pretty simple one is [Parsedown](https://parsedown.org/), if you want more features
you could also use the [Commonmark parser](https://commonmark.thephpleague.com/). I will choose Parsedown here, but you
can use whatever you like.
After installing Parsedown lets write a Markdownparser interface and an implementation using parsedown.
We only need one function that receives a string of Markdown and returns the HTML representation (as a string as well).
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Template;
interface MarkdownParser
{
public function parse(string $markdown): string;
}
```
By the namespace you will already have guessed that I placed in interface in a file calles MarkdownParser.php in
the src/Template folder. Let's put our Parsedown implementation right next to it in a file called ParsedownParser.php
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Template;
use Parsedown;
final class ParsedownRenderer implements MarkdownParser
{
public function __construct(private Parsedown $parser)
{
}
public function parse(string $markdown): string
{
return $this->parser->parse($markdown);
}
}
```
We could now use the ParsedownRender class directly in our actions by typehinting the classname as an argument to the
constructor or a method, but as we always want to rely on an interface instead of an implementation we need to define
the ParsedownRenderer as the correct implementation for the MarkdownRenderer interface in the dependencies file:
```php
...
\Lubian\NoFramework\Template\MarkdownParser::class => fn(\Lubian\NoFramework\Template\ParsedownParser $p) => $p,
...
```
You can test that in our "Other.php" action and try out if the Parser works and is able to render Markdown to HTML:
```php
public function someFunctionName(ResponseInterface $response, MarkdownParser $parser): ResponseInterface
{
$html = $parser->parse('This *works* **too!**');
$response->getBody()->write($html);
return $response->withStatus(200);
}
```
But we want to display complete Pages written in Markdown, it would also be neat to be able to display a list of all
available pages. For that we need a few things:
Firstly we need two new Templates, one for the list of the Pages, and the second one for displaying a single pages
content. Create a new folder in `templates/page` with to files:
`templates/page/list.html`
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Pages</title>
<link rel="stylesheet" href="https://unpkg.com/@picocss/pico@latest/css/pico.classless.min.css">
</head>
<body>
<main>
<ul>
{{#pages}}
<li>
<a href="/page/{{title}}">{{id}}: {{title}}</a>
</li>
{{/pages}}
</ul>
</main>
</body>
</html>
```
This template iterates over a provided array of pages, each element consists of the two properties: an id and a title,
those are simply displayed using an unordered list.
`templates/page/show.html`
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{title}}</title>
<link rel="stylesheet" href="https://unpkg.com/@picocss/pico@latest/css/pico.classless.min.css">
<link rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.0/styles/default.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
</head>
<body>
<main>
{{{content}}}
</main>
</body>
</html>
```
The second templates displays a single rendered markdown page. As data it expects the title and the content as array.
I used an extra bracket for the content ```{{{content}}}``` so that the Mustache-Renderer does not escape the provided
html and thereby destroys the parsed markdown.
You might have spotted that I added [Pico.css](https://picocss.com/) which is just a very small css framework to make the
pages a little nicer to look at. It mostly provides some typography styles that work great with rendered Markdown,
but you can leave that out or use any other css framework you like. There is also some Javascript that adds syntax
highlighting to the code.
After you have taken care of the templating side we can now create an new Action class with two methods to display use
our markdown files and the templates to create the pages. As we have two templates I propose to use Two methods in our
Action:
`src/Action/Page.php`
```php
function show(string $name): \Psr\Http\Message\ResponseInterface;
function list(): \Psr\Http\Message\ResponseInterface;
```
Let's define two routes. `/page` should display the overview of all pages, and if the add the name of chapter to the
route, `/page/adding-content` for example, the show action should be called with the name as a variable:
`config/routes.php`
```php
$r->addRoute('GET', '/page', [Page::class, 'list']);
$r->addRoute('GET', '/page/{page}', [Page::class, 'show']);
```
Here is my Implementation. I have added a little regex replacement in the show method that replaces the links to the
next and previous chapter so that it works with our routing configuration.
`src/Action/Page.php`
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Action;
use Lubian\NoFramework\Exception\InternalServerError;
use Lubian\NoFramework\Template\MarkdownParser;
use Lubian\NoFramework\Template\Renderer;
use Psr\Http\Message\ResponseInterface;
use function array_filter;
use function array_map;
use function array_values;
use function file_get_contents;
use function glob;
use function preg_replace;
use function str_contains;
use function str_replace;
use function substr;
class Page
{
public function __construct(
private ResponseInterface $response,
private MarkdownParser $parser,
private Renderer $renderer,
private string $pagesPath = __DIR__ . '/../../data/pages/'
) {
}
public function show(
string $page,
): ResponseInterface {
$page = array_values(
array_filter(
$this->getPages(),
fn (string $filename) => str_contains($filename, $page)
)
)[0];
$markdown = file_get_contents($page);
// fix the next and previous buttons to work with our routing
$markdown = preg_replace('/\(\d\d-/m', '(', $markdown);
$markdown = str_replace('.md)', ')', $markdown);
$page = str_replace([$this->pagesPath, '.md'], ['', ''], $page);
$data = [
'title' => substr($page, 3),
'content' => $this->parser->parse($markdown),
];
$html = $this->renderer->render('page/show', $data);
$this->response->getBody()->write($html);
return $this->response;
}
public function list(): ResponseInterface
{
$pages = array_map(function (string $page) {
$page = str_replace([$this->pagesPath, '.md'], ['', ''], $page);
return [
'id' => substr($page, 0, 2),
'title' => substr($page, 3),
];
}, $this->getPages());
$html = $this->renderer->render('page/list', ['pages' => $pages]);
$this->response->getBody()->write($html);
return $this->response;
}
/**
* @return string[]
*/
private function getPages(): array
{
$files = glob($this->pagesPath . '*.md');
if ($files === false) {
throw new InternalServerError('cannot read pages');
}
return $files;
}
}
```
You can now navigate your Browser to [localhost:1235/page][http://localhost:1235/page] and try out if everything works.
Of course this code is far from looking good. We heavily rely on the pages being files in the filesystem, and the action
should never be aware of the filesystem in the first place, also we have a lot of string replacements and other repetitive
code in the file. And phpstan is going to scream at us a lot, but if we rewrite the code to satisfy all the checks we would
add even more lines to that simple class, so lets move on to the next chapter where we move all the logic to separate
classes following our holy SOLID principles :)
[<< previous](14-middleware.md) | [next >>](16-data-repository.md)

265
16-data-repository.md Normal file
View file

@ -0,0 +1,265 @@
[<< previous](15-adding-content.md) | [next >>](17-performance.md)
## Data Repository
At the end of the last chapter I mentioned being unhappy with our Pages action, because there is too much stuff happening
there. We are firstly receiving some Arguments, then we are using those to query the filesystem for the given page,
loading the specific file from the filesystem, rendering the markdown, passing the markdown to the template renderer,
adding the resulting html to the response and then returning the response.
In order to make our page-action independent of the filesystem and move the code that is responsible for reading the
files
to a better place I want to introduce
the [Repository Pattern](https://designpatternsphp.readthedocs.io/en/latest/More/Repository/README.html).
I want to start by creating a class that represents the Data that is included in a page so that. For now, I can spot
three
distinct attributes.
* the ID (or chapter-number)
* the title (or name)
* the content
Currently, all those properties are always available, but we might later be able to create new pages and store them, but
at that point in time we are not yet aware of the new available ID, so we should leave that property nullable. This
allows
us to create an object without an id and let the code that actually saves the object to a persistent store define a
valid
id on saving.
Let's create an new Namespace called `Model` and put a `MarkdownPage.php` class in there:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Model;
class MarkdownPage
{
public function __construct(
public string $title,
public string $content,
public int|null $id = null,
) {
}
}
```
These small Model classes are one of my most favorite features in newer PHP-Versions, because they are almost as easy
to create as an on-the-fly array but give us the great benefit of type safety as well as full code completion in our
IDEs.
There is a [great blogpost](https://stitcher.io/blog/evolution-of-a-php-object) that highlights how this kind of
objects
have evolved in PHP from version 5.6 to 8.1, as I personally first started writing proper php with 5.4 it really baffles
me how far the language has evolved in these last years.
Next we can define our interface for the repository, for our current use case I see only two needed methods:
* get all pages
* get one page by name
The `all()` method should return an array of all available pages (or an empty one if there are none), and the
`byName(string $name)` method should either return exactly one page or throw a NotFound-Exception. You decide to return
`false` or `null` if no page with the given name could be found, but I personally prefer exception, as that keeps the
return type checking simpler and we can decide at what layer of the application we want to handle a miss on that
function.
With that said we can now define create a `Repository` namespace and place a `MarkdownPageRepo.php` there:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Repository;
use Lubian\NoFramework\Exception\NotFound;
use Lubian\NoFramework\Model\MarkdownPage;
interface MarkdownPageRepo
{
/** @return MarkdownPage[] */
public function all(): array;
/** @throws NotFound */
public function byName(string $name): MarkdownPage;
}
```
Now we can write an implementation for this interface and move our code from to Action there:
`src/Repository/FilesystemMarkdownPageRepo.php`
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Repository;
use Lubian\NoFramework\Exception\InternalServerError;
use Lubian\NoFramework\Exception\NotFound;
use Lubian\NoFramework\Model\MarkdownPage;
use function array_filter;
use function array_map;
use function array_values;
use function count;
use function file_get_contents;
use function glob;
use function str_replace;
use function substr;
final class FileSystemMarkdownPageRepo implements MarkdownPageRepo
{
public function __construct(
private readonly string $dataPath
) {
}
/** @inheritDoc */
public function all(): array
{
$files = glob($this->dataPath . '*.md');
if ($files === false) {
throw new InternalServerError('cannot read pages');
}
return array_map(function (string $filename) {
$content = file_get_contents($filename);
if ($content === false) {
throw new InternalServerError('cannot read pages');
}
$idAndTitle = str_replace([$this->dataPath, '.md'], ['', ''], $filename);
return new MarkdownPage(
(int) substr($idAndTitle, 0, 2),
substr($idAndTitle, 3),
$content
);
}, $files);
}
public function byName(string $name): MarkdownPage
{
$pages = array_values(
array_filter(
$this->all(),
fn (MarkdownPage $p) => $p->title === $name,
)
);
if (count($pages) !== 1) {
throw new NotFound;
}
return $pages[0];
}
}
```
With that in place we need to add the required `$pagesPath` to our settings class and add specify that in our
configuration.
`src/Settings.php`
```php
final class Settings
{
public function __construct(
public readonly string $environment,
public readonly string $dependenciesFile,
public readonly string $middlewaresFile,
public readonly string $templateDir,
public readonly string $templateExtension,
public readonly string $pagesPath,
) {
}
}
```
`config/settings.php`
```php
return new Settings(
environment: 'prod',
dependenciesFile: __DIR__ . '/dependencies.php',
middlewaresFile: __DIR__ . '/middlewares.php',
templateDir: __DIR__ . '/../templates',
templateExtension: '.html',
pagesPath: __DIR__ . '/../data/pages/',
);
```
Of course, we need to define the correct implementation for the container to choose when we are requesting the Repository
interface:
`conf/dependencies.php`
```php
MarkdownPageRepo::class => fn (FileSystemMarkdownPageRepo $r) => $r,
FileSystemMarkdownPageRepo::class => fn (Settings $s) => new FileSystemMarkdownPageRepo($s->pagesPath),
```
Now you can request the MarkdownPageRepo Interface in your page action and use the defined functions to get the
MarkdownPage
Objects. My `src/Action/Page.php` looks like this now:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Action;
use Lubian\NoFramework\Model\MarkdownPage;
use Lubian\NoFramework\Repository\MarkdownPageRepo;
use Lubian\NoFramework\Template\MarkdownParser;
use Lubian\NoFramework\Template\Renderer;
use Psr\Http\Message\ResponseInterface;
use function array_map;
use function assert;
use function is_string;
use function preg_replace;
use function str_replace;
class Page
{
public function __construct(
private ResponseInterface $response,
private MarkdownParser $parser,
private Renderer $renderer,
private MarkdownPageRepo $repo,
) {
}
public function show(
string $page,
): ResponseInterface {
$page = $this->repo->byName($page);
// fix the next and previous buttons to work with our routing
$content = preg_replace('/\(\d\d-/m', '(', $page->content);
assert(is_string($content));
$content = str_replace('.md)', ')', $content);
$data = [
'title' => $page->title,
'content' => $this->parser->parse($content),
];
$html = $this->renderer->render('page/show', $data);
$this->response->getBody()->write($html);
return $this->response;
}
public function list(): ResponseInterface
{
$pages = array_map(function (MarkdownPage $page) {
return [
'id' => $page->id,
'title' => $page->content,
];
}, $this->repo->all());
$html = $this->renderer->render('page/list', ['pages' => $pages]);
$this->response->getBody()->write($html);
return $this->response;
}
}
```
Check the page in your browser if everything still works, don't forget to run phpstan and the others fixers before
committing your changes and moving on to the next chapter.
[<< previous](15-adding-content.md) | [next >>](17-performance.md)

43
17-performance.md Normal file
View file

@ -0,0 +1,43 @@
[<< previous](16-data-repository.md) | [next >>](18-caching.md)
## Autoloading performance
Although our application is still very small, and you should not really experience any performance issues right now,
there are still some things we can already consider and take a look at. If I check the network tab in my browser it takes
about 90-400ms to show a simple rendered markdown, with is sort of ok but in my opinion way to long as we are not
really doing anything and do not connect to any external services. Mostly we are just reading around 16 markdown files,
a template, some config files here and there and parse some markdown. So that should not really take that long.
The problem is, that we heavily rely on autoload for all our class files, in the `src` folder. And there are also
quite a lot of other files in composers `vendor` directory. To understand while this is becoming we should make
ourselves familiar with how [autoloading in php](https://www.php.net/manual/en/language.oop5.autoload.php) works.
The basic idea is, that every class that php encounters has to be loaded from somewhere in the filesystem, we could
just require the files manually but that is tedious, unflexible and can often cause errors.
The problem we are now facing is that the composer autoloader has some rules to determine from where in the filesystem
a class definition might be placed, then the autoloader tries to locate a file by the namespace and classname and if it
exists includes that file.
If we only have a handful of classes that does not take a lot of time, but as we are growing with our application this
easily takes longer than necessary, but fortunately composer has some options to speed up the class loading.
Take a few minutes to read the documentation about [composer autoloader optimization](https://getcomposer.org/doc/articles/autoloader-optimization.md)
You can try all 3 levels of optimizations, but we are going to stick with the first one for now, so lets create an
optimized classmap.
`composer dump-autoload -o`
After composer has finished you can start the devserver again with `composer serve` and take a look at the network tab
in your browsers devtools.
In my case the response time falls down to under an average of 30ms with some spikes in between, but all in all it looks really good.
You can also try out the different optimization levels and see if you can spot any differences.
Although the composer manual states not to use the optimization in a dev environment I personally have not encountered
any errors with the first level of optimizations, so we can use that level here. If you add the line from the documentation
to your `composer.json` so that the autoloader gets optimized everytime we install new packages.
[<< previous](16-data-repository.md) | [next >>](18-caching.md)

252
18-caching.md Normal file
View file

@ -0,0 +1,252 @@
[<< previous](17-performance.md) | [next >>](19-database.md)
**DISClAIMER** I do not really have a lot of experience when it comes to caching, so this chapter is mostly some random
thoughts and ideas I wanted to explore when writing this tutorial, you should definitely take everything that is being
said here with caution and try to read up on some other sources. But that holds true for the whole tutorial anyway :)
## Caching
In the last chapter we greatly improved the perfomance for the lookup of all our classfiles, but currently we do not
have any real bottlenecks in our application like complex queries.
But in a real application we are going to execute some really heavy and time intensive database queries that can take
quite a while to be completed.
We can simulate that by adding a simple delay in our `FileSystemMarkdownPageRepo`.
```php
return array_map(function (string $filename) {
usleep(rand(100, 400) * 1000);
$content = file_get_contents($filename);
if ($content === false) {
throw new InternalServerError('cannot read pages');
}
$idAndTitle = str_replace([$this->dataPath, '.md'], ['', ''], $filename);
return new MarkdownPage(
(int) substr($idAndTitle, 0, 2),
substr($idAndTitle, 3),
$content
);
});
```
Here I added a function that pauses the scripts execution for a random time between 100 and 400ms for every markdownpage
in every call of the `all()` method.
If you open any page or even the listAction in you browser you will see, that it takes quite a time to render that page.
Although this is a silly example we do not really need to query the database on every request, so lets add a way to cache
the database results between requests.
The PHP-Community has already adressed the issue of having easy to use access to cache libraries, there is the
[PSR-6 Caching Interface](https://www.php-fig.org/psr/psr-6) which gives us easy access to many different implementations,
then there is also a much simpler [PSR-16 Simple Cache](https://www.php-fig.org/psr/psr-16) which makes the use even more
easy, and most Caching Libraries implement Both interfaces anyway. You would think that this is more than enough solutions
to satisfy all the Caching needs around, but the Symfony People decided that Caching should be even simpler and easier
to use and defined their own [Interface](https://symfony.com/doc/current/components/cache.html#cache-component-contracts)
which only needs two methods. You should definitely take a look at the linked documentation as it really blew my mind
when I first encountered it.
The basic idea is that you provide a callback that computes the requested value. The Cache implementation then checks
if it already has the value stored somewhere and if it doesnt it just executes the callback and stores the value for
future calls.
It is really simple and great to use. In a real world application you should definitely use that or a PSR-16 implementation
but for this tutorial I wanted to roll out my own solution, so here we go.
As always we are going to define an interface first, I am going to call it EasyCache and place it in the `Service/Cache`
namespace. I will require only one method which is base on the Symfony Cache Contract, and hast a key, a callback, and
the duration that the item should be cached as arguments.
```php
<?php
declare(strict_types=1);
namespace Lubian\NoFramework\Service\Cache;
interface EasyCache
{
/** @param callable(): mixed $callback */
public function get(string $key, callable $callback, int $ttl = 0): mixed;
}
```
For the implementation I am going to use the [APCu Extension](https://www.php.net/manual/en/ref.apcu.php) for PHP, but
if you are particularly adventurous you can write an implementation using memcache, the filesystem, a database, redis
or whatever you desire.
For the sake of writing as less code as possible here is my simple `ApcuCache.php`
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Service\Cache;
use function apcu_add;
use function apcu_fetch;
final class ApcuCache implements EasyCache
{
public function get(string $key, callable $callback, int $ttl = 0): mixed
{
$success = false;
$result = apcu_fetch($key, $success);
if ($success === true) {
return $result;
}
$result = $callback();
apcu_add($key, $result, $ttl);
return $result;
}
}
```
Now that we have a usable implementation for our cache we can write an implementation of our `MarkdownPageRepo` interface
that usese the Cache and a Repository implementation to speed up the time exepensive calls.
So lets create a new class called `CachedMarkdownPageRepo`:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Repository;
use Lubian\NoFramework\Model\MarkdownPage;
use Lubian\NoFramework\Service\Cache\EasyCache;
use function base64_encode;
final class CachedMarkdownPageRepo implements MarkdownPageRepo
{
public function __construct(
private EasyCache $cache,
private MarkdownPageRepo $repo,
) {
}
/**
* @inheritDoc
*/
public function all(): array
{
$key = base64_encode(self::class . 'all');
return $this->cache->get(
$key,
fn () => $this->repo->all(),
300
);
}
public function byName(string $name): MarkdownPage
{
$key = base64_encode(self::class . 'byName' . $name);
return $this->cache->get(
$key,
fn () => $this->repo->byName($name),
300
);
}
}
```
This simple wrapper just requires an EasyCache implementation and a MarkdownPageRepo in the constructor and uses them
to cache all queries for 5 minutes. The beauty is that we are not dependent on any implementation here, so we can switch
out the Repository or the Cache at any point down the road if we want to.
In order to use that we need to update our `config/dependencies.php` to add an alias for the EasyCache interface as well
as defining our CachedMarkdownPageRepo as implementation for the MarkdownPageRepo interface:
```php
MarkdownPageRepo::class => fn (CachedMarkdownPageRepo $r) => $r,
EasyCache::class => fn (ApcuCache $c) => $c,
```
If we try to access our webpage now, we are getting an error, as PHP-DI has detected a circular dependency that cannot
be autowired.
The Problem is that our CachedMarkdownPageRepo ist defined as the implementation for the MarkdownPageRepo, but it also
requires that exact interface as a dependency. To resolve this issue we need to manually tell the container how to build
the CachedMarkdownPageRepo by adding another line to the `config/dependencies.php` file:
```php
CachedMarkdownPageRepo::class => fn (EasyCache $c, FileSystemMarkdownPageRepo $r) => new CachedMarkdownPageRepo($c, $r),
```
Here we explicitly require the FileSystemMarkdownPageRepo and us that to create the CachedMarkdownPageRepo object.
When you now navigate to the pages list or to a specific page the first load should take a while (because of our added delay)
but the following request should be answered blazingly fast.
Before moving on to the next chapter we can take the caching approach even further, in the middleware chapter I talked
about a simple CachingMiddleware that caches all the GET-Request for some seconds, as they should not change that often,
and we can bypass most of our application logic if we just complelety cache away the responses our application generates,
and return them quite early in our Middleware-Pipeline befor the router gets called, or the invoker calls the action,
which itself uses some other services to fetch all the needed data.
We will introduce a new `Middleware` namespace to place our `Cache.php` middleware:
```php
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Middleware;
use Laminas\Diactoros\Response\Serializer;
use Lubian\NoFramework\Service\Cache\EasyCache;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use function base64_encode;
final class Cache implements MiddlewareInterface
{
public function __construct(
private readonly EasyCache $cache,
private readonly Serializer $serializer,
) {
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if ($request->getMethod() !== 'GET') {
return $handler->handle($request);
}
$keyHash = base64_encode($request->getUri()->getPath());
$result = $this->cache->get(
$keyHash,
fn () => $this->serializer::toString($handler->handle($request)),
300
);
return $this->serializer::fromString($result);
}
}
```
The code is quite straight forward, but you might be confused by the Responseserializer I have added here, we need this
because the response body is a stream object, which doesnt always gets serialized correctly, therefore I use a class from
the laminas project to to all the heavy lifting for us.
We need to add the now middleware to the `config/middlewares.php` file.
```php
<?php declare(strict_types=1);
use Lubian\NoFramework\Http\RouteMiddleware;
use Lubian\NoFramework\Middleware\Cache;
use Middlewares\TrailingSlash;
use Middlewares\Whoops;
return [
Whoops::class,
Cache::class,
TrailingSlash::class,
RouteMiddleware::class,
];
```
You can now use your browser to look if everything works as expected.
**Disclaimer** in a real application you would take some more consideration when it comes to caching and this simple
response cache would quickly get in you way, but as I said earlier this chapter was mostly me playing around with some
ideas I had in writing this tutorial.
[<< previous](17-performance.md) | [next >>](19-database.md)

View file

@ -1,54 +0,0 @@
[<< previous](1-front-controller.md) | [next >>](3-error-handler.md)
### Composer
[Composer](https://getcomposer.org/) is a dependency manager for PHP.
Just because you are not using a framework does not mean you will have to reinvent the wheel every time you want to do something. With Composer, you can install third-party libraries for your application.
If you don't have Composer installed already, head over to the website and install it. You can find Composer packages for your project on [Packagist](https://packagist.org/).
Create a new file in your project root folder called `composer.json`. This is the Composer configuration file that will be used to configure your project and its dependencies. It must be valid JSON or Composer will fail.
Add the following content to the file:
```
{
"name": "Project name",
"description": "Your project description",
"keywords": ["Your keyword", "Another keyword"],
"license": "MIT",
"authors": [
{
"name": "Your Name",
"email": "your@email.com",
"role": "Creator / Main Developer"
}
],
"require": {
"php": ">=5.5.0"
},
"autoload": {
"psr-4": {
"Example\\": "src/"
}
}
}
```
In the autoload part you can see that I am using the `Example` namespace for the project. You can use whatever fits your project there, but from now on I will always use the `Example` namespace in my examples. Just replace it with your namespace in your own code.
Open a new console window and navigate into your project root folder. There run `composer update`.
Composer creates a `composer.lock` file that locks in your dependencies and a vendor directory. To remove those from your Git repository, add a new file in your project root folder called `.gitignore` with the following content:
```
composer.lock
vendor/
```
This will exclude the included file and folder from your commits. For which now would be a good time, by the way.
Now you have successfully created an empty playground which you can use to set up your project.
[<< previous](1-front-controller.md) | [next >>](3-error-handler.md)

View file

@ -1,65 +0,0 @@
[<< previous](2-composer.md) | [next >>](4-http.md)
### Error Handler
An error handler allows you to customize what happens if your code results in an error.
A nice error page with a lot of information for debugging goes a long way during development. So the first package for your application will take care of that.
I like [filp/whoops](https://github.com/filp/whoops), so I will show how you can install that package for your project. If you prefer another package, feel free to install that one. This is the beauty of programming without a framework, you have total control over your project.
An alternative package would be: [PHP-Error](https://github.com/JosephLenton/PHP-Error)
To install a new package, open up your `composer.json` and add the package to the require part. It should now look like this:
```
"require": {
"php": ">=5.5.0",
"filp/whoops": ">=1.1.2"
},
```
Now run `composer update` in your console and it will be installed.
But you can't use it yet. PHP won't know where to find the files for the classes. For this you will need an autoloader, ideally a [PSR-4](http://www.php-fig.org/psr/psr-4/) autoloader. Composer already takes care of this for you, so you only have to add a `require '../vendor/autoload.php';` to your `Bootstrap.php`.
Now before you start adding the error handler code to the
**Important:** Never show any errors in your production environment. A stack trace or even just a simple error message can help someone to gain access to your system. Always show a user friendly error page instead and send an email to yourself, write to a log or something similar. So only you can see the errors in the production environment.
For development that does not make sense though -- you want a nice error page. The solution is to have an environment switch in your code. For now you can just set it to `development`.
Then after the error handler registration, throw an `Exception` to test if everything is working correctly. Your `Bootstrap.php` should now look similar to this:
```
<?php
namespace Example;
require '../vendor/autoload.php';
error_reporting(E_ALL);
$environment = 'development';
/**
* Register the error handler
*/
$woops = new \Whoops\Run;
if ($environment !== 'production') {
$woops->pushHandler(new \Whoops\Handler\PrettyPageHandler);
} else {
$woops->pushHandler(function($e){
echo 'Friendly error page and send an email to the developer';
});
}
$woops->register();
throw new \Exception;
```
You should now see a error page with the line highlighted where you throw the exception. If not, go back and debug until you get it working. Now would also be a good time for another commit.
[<< previous](2-composer.md) | [next >>](4-http.md)

View file

@ -1,64 +0,0 @@
[<< previous](3-error-handler.md) | [next >>](5-router.md)
### HTTP
PHP already has a few things built in to make working with HTTP easier. For example there are the [superglobals](http://php.net/manual/en/language.variables.superglobals.php) that contain the request information.
These are good if you just want to get a small script up and running without much thought on maintenance. However, if you want to write clean, maintainable, [SOLID](http://en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29) code, then you will want a class with a nice object-oriented interface that you can use in your application.
Once again, you don't have to reinvent the wheel and just install a package. I decided to write my own [HTTP component](https://github.com/PatrickLouys/http) because I did not like the existing components, but you don't have to do the same.
Some alternatives: [Symfony HttpFoundation](https://github.com/symfony/HttpFoundation), [Nette HTTP Component](https://github.com/nette/http), [Aura Http](https://github.com/auraphp/Aura.Http), [sabre/http](https://github.com/fruux/sabre-http)
In this tutorial I will use my own HTTP component, but of course you can use whichever package you like most. Just change the code accordingly.
Again, edit the `composer.json` to add the new component and then run `composer update`:
```
"require": {
"php": ">=5.5.0",
"filp/whoops": ">=1.1.2",
"patricklouys/http": ">=1.0.2"
},
```
Now you can add the following below your error handler code in your `Bootstrap.php` (and don't forget to remove the exception):
```
$request = new \Http\HttpRequest($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
$response = new \Http\HttpResponse;
```
This sets up the `Request` and `Response` objects that you can use in your other classes to get request data and send a response back to the browser.
To actually send something back, you will also need to add the following snippet at the end of your `Bootstrap.php` file:
```
foreach ($response->getHeaders() as $header) {
header($header);
}
echo $response->getContent();
```
This will send the response data to the browser. If you don't do this, nothing happens as the `Response` object only stores data. This is handled differently by most other HTTP components where the classes send data back to the browser as a side-effect, so keep that in mind if you use another component.
Right now it is just sending an empty response back to the browser with the status code `200`; to change that, add the following code between the code snippets from above:
```
$content = '<h1>Hello World</h1>';
$response->setContent($content);
```
If you want to try a 404 error, use the following code:
```
$response->setContent('404 - Page not found');
$response->setStatusCode(404);
```
Remember that the object is only storing data, so you if you set multiple status codes before you send the response, only the last one will be applied.
I will show you in later parts how to use the different features of the components. In the meantime, feel free to read the [documentation](https://github.com/PatrickLouys/http) or the source code if you want to find out how something works.
[<< previous](3-error-handler.md) | [next >>](5-router.md)

View file

@ -1,79 +0,0 @@
[<< previous](4-http.md) | [next >>](6-controllers.md)
### Router
A router dispatches to different handlers depending on rules that you have set up.
With your current setup it does not matter what URL is used to access the application, it will always result in the same response. So let's fix that now.
I will use [FastRoute](https://github.com/nikic/FastRoute) in this tutorial. But as always, you can pick your own favorite package.
Alternative packages: [symfony/Routing](https://github.com/symfony/Routing), [Aura.Router](https://github.com/auraphp/Aura.Router), [fuelphp/routing](https://github.com/fuelphp/routing), [Klein](https://github.com/chriso/klein.php)
By now you know how to install Composer packages, so I will leave that to you.
Now add this code block to your `Bootstrap.php` file where you added the 'hello world' message in the last part.
```
$dispatcher = \FastRoute\simpleDispatcher(function (\FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/hello-world', function () {
echo 'Hello World';
});
$r->addRoute('GET', '/another-route', function () {
echo 'This works too';
});
});
$routeInfo = $dispatcher->dispatch($request->getMethod(), $request->getUri());
switch ($routeInfo[0]) {
case \FastRoute\Dispatcher::NOT_FOUND:
$response->setContent('404 - Page not found');
$response->setStatusCode(404);
break;
case \FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
$response->setContent('405 - Method not allowed');
$response->setStatusCode(405);
break;
case \FastRoute\Dispatcher::FOUND:
$handler = $routeInfo[1];
$vars = $routeInfo[2];
call_user_func($handler, $vars);
break;
}
```
In the first part of the code, you are registering the available routes for you application. In the second part, the dispatcher gets called and the appropriate part of the switch statement will be executed. If a route was found, the handler callable will be executed.
This setup might work for really small applications, but once you start adding a few routes your bootstrap file will quickly get cluttered. So let's move them out into a separate file.
Create a `Routes.php` file in the `src/` folder. It should look like this:
```
<?php
return [
['GET', '/hello-world', function () {
echo 'Hello World';
}],
['GET', '/another-route', function () {
echo 'This works too';
}],
];
```
Now let's rewrite the route collection part to use the `Routes.php` file.
```
$routeDefinitionCallback = function (\FastRoute\RouteCollector $r) {
$routes = include('Routes.php');
foreach ($routes as $route) {
$r->addRoute($route[0], $route[1], $route[2]);
}
};
$dispatcher = \FastRoute\simpleDispatcher($routeDefinitionCallback);
```
This is already an improvement, but now all the handler code is in the `Routers.php` file. This is not optimal, so let's fix that in the next part.
[<< previous](4-http.md) | [next >>](6-controllers.md)

View file

@ -1,7 +0,0 @@
[<< previous](5-router.md)
### Controllers
When I talk about a controller in this tutorial then I am just referring to a class that has handler methods. I am not talking about [MVC (Model-View-Controller)](http://martinfowler.com/eaaCatalog/modelViewController.html) controllers. MVC can't be implemented properly in PHP anyway, at least not in the way it was originally conceived. So forget about MVC and instead let's worry about [separation of concerns](http://en.wikipedia.org/wiki/Separation_of_concerns).
to be continued...

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Patrick Louys
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,23 +1,37 @@
## Create a PHP application without a framework
# Create a PHP application without a Framework
### Introduction
Hello and welcome to this tutorial with helps you in understanding how to write complex apps without the help of
a framework. This tutorial is not for people who have never written PHP before, you should at least have some
experience with object oriented PHP and be able to look at the official PHP-Documentation to figure out what
a function or class we are using does.
If you are really new to the language, this is not for you. This tutorial is aimed at people who have grasped the basics of PHP and know a little bit about object-oriented programming.
I often hear people talking about frameworks as a solution to all the problems that you have in software development.
But in my opinion its even worse to use a framework if you do not know what you are doing, because often are fighting
more against the framework than actually solving the problem you should be working on. Even if you know what you are
doing i think it is good to get to know how the frameworks you are using work under the hood and what challenges they
actually solve for you.
You should at least heard of [SOLID](http://en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29). If you are not familiar with it, now would be a good time to familiarize yourself with the principles before you start with the tutorial.
## Credit:
I saw a lot of people coming into the Stack Overflow PHP chatroom and asking if framework X is any good. Most of the time the answer was that they should just use PHP and not a framework to build their application. But many are overwhelmed by this and don't know where to start.
This tutorial is based on the great [tutorial by Patrick Louys](https://github.com/PatrickLouys/no-framework-tutorial).
My version is way more opiniated and uses some newer PHP features. But you should still check out his tutorial which is
still very great and helped me personally a lot in taking the next step in my knowledge about PHP development. There is
also an [amazing book](https://patricklouys.com/professional-php/) which expands on the topics covered in this tutorial.
So my goal with this is to provide an easy resource that people can be pointed to. In most cases a framework does not make sense and writing an application from scratch with the help of some third party packages is much, much easier than some people think.
## Getting started.
So let's get started right away with the [first part](1-front-controller.md).
As I am using a fairly new version of PHP in this tutorial I have added a Vagrantfile to this tutorial. If you do not
have PHP8.1 installed on your computer you can use the following commands to try out all the examples:
### Parts
```shell
vagrant up
vagrant ssh
cd app
```
I have exposed the port 1235 to be used in the VM, if you would like to use another one you are free to modify the
Vagrantfile.
[Start](01-front-controller.md)
1. [Front Controller](1-front-controller.md)
2. [Composer](2-composer.md)
3. [Error Handler](3-error-handler.md)
4. [HTTP](4-http.md)
5. [Router](5-router.md)
6. [Controllers](6-controllers.md)
7. Dependency Injector

28
Vagrantfile vendored Normal file
View file

@ -0,0 +1,28 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "archlinux/archlinux"
config.vm.provider "virtualbox" do |v|
v.memory = 512
v.cpus = 2
end
config.vm.network "forwarded_port", guest: 1235, host: 1235
config.vm.network "forwarded_port", guest: 22, host: 2200, id: 'ssh'
config.vm.synced_folder "./app", "/home/vagrant/app/"
config.ssh.username = 'vagrant'
config.ssh.password = 'vagrant'
config.vm.provision "shell", inline: <<-SHELL
pacman -Syu --noconfirm
pacman -S --noconfirm php php-sqlite php-intl php-sodium php-apcu composer xdebug vim
echo '127.0.0.1 localhost' >> /etc/hosts
echo -e 'extension=pdo_sqlite\nextenstion=sqlite3\n' >> /etc/php/conf.d/tutorial.ini
echo -e 'extension=apcu\nzend_extension=opcache\n' >> /etc/php/conf.d/tutorial.ini
echo -e 'zend_extension=xdebug\nxdebug.client_host=10.0.2.2\n' >> /etc/php/conf.d/tutorial.ini
echo -e 'xdebug.client_port=9003\nxdebug.mode=debug\n' >> /etc/php/conf.d/tutorial.ini
echo -e 'zend.assertions=1\n' >> /etc/php/conf.d/tutorial.ini
echo -e 'opcache.enable=1\nopcache.enable_cli=1\n' >> /etc/php/conf.d/tutorial.ini
echo -e 'acp.enable=1\napc.enable_cli=1\n' >> /etc/php/conf.d/tutorial.ini
echo -e 'extension=intl\n' >> /etc/php/conf.d/tutorial.ini
SHELL
end

View file

@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../Bootstrap.php';

View file

@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
echo 'Hello world!';

View file

@ -0,0 +1,19 @@
{
"name": "lubiana/no-framework",
"type": "project",
"license": "MIT",
"autoload": {
"psr-4": {
"Lubiana\\NoFramework\\": "src/"
}
},
"authors": [
{
"name": "lubiana",
"email": "lubiana@hannover.ccc.de"
}
],
"require": {
"php": "^8.4"
}
}

View file

@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../Bootstrap.php';

View file

@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
namespace Lubiana\NoFramework;
echo 'Hello world!';

View file

@ -0,0 +1,20 @@
{
"name": "lubiana/no-framework",
"type": "project",
"license": "MIT",
"autoload": {
"psr-4": {
"Lubiana\\NoFramework\\": "src/"
}
},
"authors": [
{
"name": "lubiana",
"email": "lubiana@hannover.ccc.de"
}
],
"require": {
"php": "^8.4",
"filp/whoops": "^2.18"
}
}

View file

@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../src/Bootstrap.php';

View file

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Lubiana\NoFramework;
use Throwable;
use Whoops\Handler\CallbackHandler;
use Whoops\Handler\PrettyPageHandler;
use Whoops\Run;
require __DIR__ . '/../vendor/autoload.php';
$environment = getenv('APP_ENV') ?: 'dev';
$whoops = new Run;
$whoops->pushHandler(
new CallbackHandler(
function (Throwable $e) use ($environment) {
if ($environment !== 'dev') {
http_response_code(500);
echo 'Whoops';
}
error_log(<<<TXT
Error: {$e->getMessage()}
{$e->getTraceAsString()}
TXT
);
}
)
);
if ($environment === 'dev') {
$whoops->pushHandler(new PrettyPageHandler);
}
$whoops->register();
throw new \Exception('Hello world');

View file

@ -0,0 +1,45 @@
{
"name": "lubiana/no-framework",
"autoload": {
"psr-4": {
"Lubiana\\NoFramework\\": "src/"
}
},
"authors": [
{
"name": "example",
"email": "test@example.com"
}
],
"require": {
"php": ">=8.1",
"filp/whoops": "^2.14"
},
"require-dev": {
"phpstan/phpstan": "^1.6",
"symfony/var-dumper": "^6.0",
"slevomat/coding-standard": "^7.2",
"symplify/easy-coding-standard": "^10.2",
"rector/rector": "^0.12.23",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-strict-rules": "^1.2",
"thecodingmachine/phpstan-strict-rules": "^1.0"
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"phpstan/extension-installer": true
}
},
"scripts": {
"serve": [
"Composer\\Config::disableProcessTimeout",
"php -S 0.0.0.0:1235 -t public"
],
"phpstan": "./vendor/bin/phpstan analyze",
"baseline": "./vendor/bin/phpstan analyze --generate-baseline",
"check": "./vendor/bin/ecs",
"fix": "./vendor/bin/ecs --fix",
"rector": "./vendor/bin/rector process"
}
}

872
implementation/04/composer.lock generated Normal file
View file

@ -0,0 +1,872 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "92e03e1fbb1466733bb7150c4a0dec9f",
"packages": [
{
"name": "filp/whoops",
"version": "2.14.5",
"source": {
"type": "git",
"url": "https://github.com/filp/whoops.git",
"reference": "a63e5e8f26ebbebf8ed3c5c691637325512eb0dc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filp/whoops/zipball/a63e5e8f26ebbebf8ed3c5c691637325512eb0dc",
"reference": "a63e5e8f26ebbebf8ed3c5c691637325512eb0dc",
"shasum": ""
},
"require": {
"php": "^5.5.9 || ^7.0 || ^8.0",
"psr/log": "^1.0.1 || ^2.0 || ^3.0"
},
"require-dev": {
"mockery/mockery": "^0.9 || ^1.0",
"phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.3",
"symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0"
},
"suggest": {
"symfony/var-dumper": "Pretty print complex values better with var-dumper available",
"whoops/soap": "Formats errors as SOAP responses"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.7-dev"
}
},
"autoload": {
"psr-4": {
"Whoops\\": "src/Whoops/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Filipe Dobreira",
"homepage": "https://github.com/filp",
"role": "Developer"
}
],
"description": "php error handling for cool kids",
"homepage": "https://filp.github.io/whoops/",
"keywords": [
"error",
"exception",
"handling",
"library",
"throwable",
"whoops"
],
"support": {
"issues": "https://github.com/filp/whoops/issues",
"source": "https://github.com/filp/whoops/tree/2.14.5"
},
"funding": [
{
"url": "https://github.com/denis-sokolov",
"type": "github"
}
],
"time": "2022-01-07T12:00:00+00:00"
},
{
"name": "psr/log",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001",
"reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001",
"shasum": ""
},
"require": {
"php": ">=8.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Log\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"homepage": "https://github.com/php-fig/log",
"keywords": [
"log",
"psr",
"psr-3"
],
"support": {
"source": "https://github.com/php-fig/log/tree/3.0.0"
},
"time": "2021-07-14T16:46:02+00:00"
}
],
"packages-dev": [
{
"name": "dealerdirect/phpcodesniffer-composer-installer",
"version": "v0.7.2",
"source": {
"type": "git",
"url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git",
"reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db",
"reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db",
"shasum": ""
},
"require": {
"composer-plugin-api": "^1.0 || ^2.0",
"php": ">=5.3",
"squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0"
},
"require-dev": {
"composer/composer": "*",
"php-parallel-lint/php-parallel-lint": "^1.3.1",
"phpcompatibility/php-compatibility": "^9.0"
},
"type": "composer-plugin",
"extra": {
"class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin"
},
"autoload": {
"psr-4": {
"Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Franck Nijhof",
"email": "franck.nijhof@dealerdirect.com",
"homepage": "http://www.frenck.nl",
"role": "Developer / IT Manager"
},
{
"name": "Contributors",
"homepage": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer/graphs/contributors"
}
],
"description": "PHP_CodeSniffer Standards Composer Installer Plugin",
"homepage": "http://www.dealerdirect.com",
"keywords": [
"PHPCodeSniffer",
"PHP_CodeSniffer",
"code quality",
"codesniffer",
"composer",
"installer",
"phpcbf",
"phpcs",
"plugin",
"qa",
"quality",
"standard",
"standards",
"style guide",
"stylecheck",
"tests"
],
"support": {
"issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues",
"source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer"
},
"time": "2022-02-04T12:51:07+00:00"
},
{
"name": "phpstan/extension-installer",
"version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/phpstan/extension-installer.git",
"reference": "66c7adc9dfa38b6b5838a9fb728b68a7d8348051"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/extension-installer/zipball/66c7adc9dfa38b6b5838a9fb728b68a7d8348051",
"reference": "66c7adc9dfa38b6b5838a9fb728b68a7d8348051",
"shasum": ""
},
"require": {
"composer-plugin-api": "^1.1 || ^2.0",
"php": "^7.1 || ^8.0",
"phpstan/phpstan": ">=0.11.6"
},
"require-dev": {
"composer/composer": "^1.8",
"phing/phing": "^2.16.3",
"php-parallel-lint/php-parallel-lint": "^1.2.0",
"phpstan/phpstan-strict-rules": "^0.11 || ^0.12"
},
"type": "composer-plugin",
"extra": {
"class": "PHPStan\\ExtensionInstaller\\Plugin"
},
"autoload": {
"psr-4": {
"PHPStan\\ExtensionInstaller\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Composer plugin for automatic installation of PHPStan extensions",
"support": {
"issues": "https://github.com/phpstan/extension-installer/issues",
"source": "https://github.com/phpstan/extension-installer/tree/1.1.0"
},
"time": "2020-12-13T13:06:13+00:00"
},
{
"name": "phpstan/phpdoc-parser",
"version": "1.5.1",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
"reference": "981cc368a216c988e862a75e526b6076987d1b50"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/981cc368a216c988e862a75e526b6076987d1b50",
"reference": "981cc368a216c988e862a75e526b6076987d1b50",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^1.5",
"phpstan/phpstan-strict-rules": "^1.0",
"phpunit/phpunit": "^9.5",
"symfony/process": "^5.2"
},
"type": "library",
"autoload": {
"psr-4": {
"PHPStan\\PhpDocParser\\": [
"src/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
"source": "https://github.com/phpstan/phpdoc-parser/tree/1.5.1"
},
"time": "2022-05-05T11:32:40+00:00"
},
{
"name": "phpstan/phpstan",
"version": "1.6.8",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "d76498c5531232cb8386ceb6004f7e013138d3ba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/d76498c5531232cb8386ceb6004f7e013138d3ba",
"reference": "d76498c5531232cb8386ceb6004f7e013138d3ba",
"shasum": ""
},
"require": {
"php": "^7.2|^8.0"
},
"conflict": {
"phpstan/phpstan-shim": "*"
},
"bin": [
"phpstan",
"phpstan.phar"
],
"type": "library",
"autoload": {
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPStan - PHP Static Analysis Tool",
"support": {
"issues": "https://github.com/phpstan/phpstan/issues",
"source": "https://github.com/phpstan/phpstan/tree/1.6.8"
},
"funding": [
{
"url": "https://github.com/ondrejmirtes",
"type": "github"
},
{
"url": "https://github.com/phpstan",
"type": "github"
},
{
"url": "https://www.patreon.com/phpstan",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan",
"type": "tidelift"
}
],
"time": "2022-05-10T06:54:21+00:00"
},
{
"name": "phpstan/phpstan-strict-rules",
"version": "1.2.3",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-strict-rules.git",
"reference": "0c82c96f2a55d8b91bbc7ee6512c94f68a206b43"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/0c82c96f2a55d8b91bbc7ee6512c94f68a206b43",
"reference": "0c82c96f2a55d8b91bbc7ee6512c94f68a206b43",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0",
"phpstan/phpstan": "^1.6.3"
},
"require-dev": {
"nikic/php-parser": "^4.13.0",
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^9.5"
},
"type": "phpstan-extension",
"extra": {
"phpstan": {
"includes": [
"rules.neon"
]
}
},
"autoload": {
"psr-4": {
"PHPStan\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Extra strict and opinionated rules for PHPStan",
"support": {
"issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.2.3"
},
"time": "2022-05-04T15:20:40+00:00"
},
{
"name": "rector/rector",
"version": "0.12.23",
"source": {
"type": "git",
"url": "https://github.com/rectorphp/rector.git",
"reference": "690b31768b322db886b35845f8452025eba2cacb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/rectorphp/rector/zipball/690b31768b322db886b35845f8452025eba2cacb",
"reference": "690b31768b322db886b35845f8452025eba2cacb",
"shasum": ""
},
"require": {
"php": "^7.2|^8.0",
"phpstan/phpstan": "^1.6"
},
"conflict": {
"phpstan/phpdoc-parser": "<1.2",
"rector/rector-cakephp": "*",
"rector/rector-doctrine": "*",
"rector/rector-laravel": "*",
"rector/rector-nette": "*",
"rector/rector-phpoffice": "*",
"rector/rector-phpunit": "*",
"rector/rector-prefixed": "*",
"rector/rector-symfony": "*"
},
"bin": [
"bin/rector"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "0.12-dev"
}
},
"autoload": {
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Instant Upgrade and Automated Refactoring of any PHP code",
"support": {
"issues": "https://github.com/rectorphp/rector/issues",
"source": "https://github.com/rectorphp/rector/tree/0.12.23"
},
"funding": [
{
"url": "https://github.com/tomasvotruba",
"type": "github"
}
],
"time": "2022-05-01T15:50:16+00:00"
},
{
"name": "slevomat/coding-standard",
"version": "7.2.0",
"source": {
"type": "git",
"url": "https://github.com/slevomat/coding-standard.git",
"reference": "b4f96a8beea515d2d89141b7b9ad72f526d84071"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/slevomat/coding-standard/zipball/b4f96a8beea515d2d89141b7b9ad72f526d84071",
"reference": "b4f96a8beea515d2d89141b7b9ad72f526d84071",
"shasum": ""
},
"require": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7",
"php": "^7.2 || ^8.0",
"phpstan/phpdoc-parser": "^1.5.1",
"squizlabs/php_codesniffer": "^3.6.2"
},
"require-dev": {
"phing/phing": "2.17.3",
"php-parallel-lint/php-parallel-lint": "1.3.2",
"phpstan/phpstan": "1.4.10|1.6.7",
"phpstan/phpstan-deprecation-rules": "1.0.0",
"phpstan/phpstan-phpunit": "1.0.0|1.1.1",
"phpstan/phpstan-strict-rules": "1.2.3",
"phpunit/phpunit": "7.5.20|8.5.21|9.5.20"
},
"type": "phpcodesniffer-standard",
"extra": {
"branch-alias": {
"dev-master": "7.x-dev"
}
},
"autoload": {
"psr-4": {
"SlevomatCodingStandard\\": "SlevomatCodingStandard"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.",
"support": {
"issues": "https://github.com/slevomat/coding-standard/issues",
"source": "https://github.com/slevomat/coding-standard/tree/7.2.0"
},
"funding": [
{
"url": "https://github.com/kukulich",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard",
"type": "tidelift"
}
],
"time": "2022-05-06T10:58:42+00:00"
},
{
"name": "squizlabs/php_codesniffer",
"version": "3.6.2",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
"reference": "5e4e71592f69da17871dba6e80dd51bce74a351a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/5e4e71592f69da17871dba6e80dd51bce74a351a",
"reference": "5e4e71592f69da17871dba6e80dd51bce74a351a",
"shasum": ""
},
"require": {
"ext-simplexml": "*",
"ext-tokenizer": "*",
"ext-xmlwriter": "*",
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
},
"bin": [
"bin/phpcs",
"bin/phpcbf"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Greg Sherwood",
"role": "lead"
}
],
"description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
"homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
"keywords": [
"phpcs",
"standards"
],
"support": {
"issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
"source": "https://github.com/squizlabs/PHP_CodeSniffer",
"wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
},
"time": "2021-12-12T21:44:58+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.25.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825",
"reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.25.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-11-30T18:21:41+00:00"
},
{
"name": "symfony/var-dumper",
"version": "v6.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "fa61dfb4bd3068df2492013dc65f3190e9f550c0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/fa61dfb4bd3068df2492013dc65f3190e9f550c0",
"reference": "fa61dfb4bd3068df2492013dc65f3190e9f550c0",
"shasum": ""
},
"require": {
"php": ">=8.0.2",
"symfony/polyfill-mbstring": "~1.0"
},
"conflict": {
"phpunit/phpunit": "<5.4.3",
"symfony/console": "<5.4"
},
"require-dev": {
"ext-iconv": "*",
"symfony/console": "^5.4|^6.0",
"symfony/process": "^5.4|^6.0",
"symfony/uid": "^5.4|^6.0",
"twig/twig": "^2.13|^3.0.4"
},
"suggest": {
"ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).",
"ext-intl": "To show region name in time zone dump",
"symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script"
},
"bin": [
"Resources/bin/var-dump-server"
],
"type": "library",
"autoload": {
"files": [
"Resources/functions/dump.php"
],
"psr-4": {
"Symfony\\Component\\VarDumper\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides mechanisms for walking through any arbitrary PHP variable",
"homepage": "https://symfony.com",
"keywords": [
"debug",
"dump"
],
"support": {
"source": "https://github.com/symfony/var-dumper/tree/v6.0.8"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-04-26T13:22:23+00:00"
},
{
"name": "symplify/easy-coding-standard",
"version": "10.2.6",
"source": {
"type": "git",
"url": "https://github.com/symplify/easy-coding-standard.git",
"reference": "8875d8cd438756c9719fcdcc3b7d0c1d06515dd5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symplify/easy-coding-standard/zipball/8875d8cd438756c9719fcdcc3b7d0c1d06515dd5",
"reference": "8875d8cd438756c9719fcdcc3b7d0c1d06515dd5",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"conflict": {
"friendsofphp/php-cs-fixer": "<3.0",
"squizlabs/php_codesniffer": "<3.6"
},
"bin": [
"bin/ecs"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "9.5-dev"
}
},
"autoload": {
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Prefixed scoped version of ECS package",
"support": {
"source": "https://github.com/symplify/easy-coding-standard/tree/10.2.6"
},
"funding": [
{
"url": "https://www.paypal.me/rectorphp",
"type": "custom"
},
{
"url": "https://github.com/tomasvotruba",
"type": "github"
}
],
"time": "2022-05-17T07:11:50+00:00"
},
{
"name": "thecodingmachine/phpstan-strict-rules",
"version": "v1.0.0",
"source": {
"type": "git",
"url": "https://github.com/thecodingmachine/phpstan-strict-rules.git",
"reference": "2ba8fa8b328c45f3b149c05def5bf96793c594b6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thecodingmachine/phpstan-strict-rules/zipball/2ba8fa8b328c45f3b149c05def5bf96793c594b6",
"reference": "2ba8fa8b328c45f3b149c05def5bf96793c594b6",
"shasum": ""
},
"require": {
"php": "^7.1|^8.0",
"phpstan/phpstan": "^1.0"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^7.1"
},
"type": "phpstan-extension",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
},
"phpstan": {
"includes": [
"phpstan-strict-rules.neon"
]
}
},
"autoload": {
"psr-4": {
"TheCodingMachine\\PHPStan\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "David Négrier",
"email": "d.negrier@thecodingmachine.com"
}
],
"description": "A set of additional rules for PHPStan based on best practices followed at TheCodingMachine",
"support": {
"issues": "https://github.com/thecodingmachine/phpstan-strict-rules/issues",
"source": "https://github.com/thecodingmachine/phpstan-strict-rules/tree/v1.0.0"
},
"time": "2021-11-08T09:10:49+00:00"
}
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=8.1"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"
}

89
implementation/04/ecs.php Normal file
View file

@ -0,0 +1,89 @@
<?php declare(strict_types=1);
use PhpCsFixer\Fixer\Import\OrderedImportsFixer;
use PhpCsFixer\Fixer\Operator\NewWithBracesFixer;
use PhpCsFixer\Fixer\PhpTag\BlankLineAfterOpeningTagFixer;
use SlevomatCodingStandard\Sniffs\Classes\ClassConstantVisibilitySniff;
use SlevomatCodingStandard\Sniffs\ControlStructures\NewWithoutParenthesesSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\AlphabeticallySortedUsesSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\DisallowGroupUseSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\MultipleUsesPerLineSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\NamespaceSpacingSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\ReferenceUsedNamesOnlySniff;
use SlevomatCodingStandard\Sniffs\Namespaces\UseSpacingSniff;
use SlevomatCodingStandard\Sniffs\TypeHints\DeclareStrictTypesSniff;
use SlevomatCodingStandard\Sniffs\TypeHints\UnionTypeHintFormatSniff;
use Symplify\EasyCodingStandard\Config\ECSConfig;
use Symplify\EasyCodingStandard\ValueObject\Set\SetList;
return static function (ECSConfig $config): void {
$config->parallel();
$config->paths([__DIR__ . '/src', __DIR__ . '/ecs.php', __DIR__ . '/rector.php']);
$config->skip([BlankLineAfterOpeningTagFixer::class, OrderedImportsFixer::class, NewWithBracesFixer::class]);
$config->sets([
SetList::PSR_12,
SetList::STRICT,
SetList::ARRAY,
SetList::SPACES,
SetList::DOCBLOCK,
SetList::CLEAN_CODE,
SetList::COMMON,
SetList::COMMENTS,
SetList::NAMESPACES,
SetList::SYMPLIFY,
SetList::CONTROL_STRUCTURES,
]);
// force visibility declaration on class constants
$config->ruleWithConfiguration(ClassConstantVisibilitySniff::class, [
'fixable' => true,
]);
// sort all use statements
$config->rules([
AlphabeticallySortedUsesSniff::class,
DisallowGroupUseSniff::class,
MultipleUsesPerLineSniff::class,
NamespaceSpacingSniff::class,
]);
// import all namespaces, and event php core functions and classes
$config->ruleWithConfiguration(
ReferenceUsedNamesOnlySniff::class,
[
'allowFallbackGlobalConstants' => false,
'allowFallbackGlobalFunctions' => false,
'allowFullyQualifiedGlobalClasses' => false,
'allowFullyQualifiedGlobalConstants' => false,
'allowFullyQualifiedGlobalFunctions' => false,
'allowFullyQualifiedNameForCollidingClasses' => true,
'allowFullyQualifiedNameForCollidingConstants' => true,
'allowFullyQualifiedNameForCollidingFunctions' => true,
'searchAnnotations' => true,
]
);
// define newlines between use statements
$config->ruleWithConfiguration(UseSpacingSniff::class, [
'linesCountBeforeFirstUse' => 1,
'linesCountBetweenUseTypes' => 1,
'linesCountAfterLastUse' => 1,
]);
// strict types declaration should be on same line as opening tag
$config->ruleWithConfiguration(DeclareStrictTypesSniff::class, [
'declareOnFirstLine' => true,
'spacesCountAroundEqualsSign' => 0,
]);
// disallow ?Foo typehint in favor of Foo|null
$config->ruleWithConfiguration(UnionTypeHintFormatSniff::class, [
'withSpaces' => 'no',
'shortNullable' => 'no',
'nullPosition' => 'last',
]);
// Remove useless parentheses in new statements
$config->rule(NewWithoutParenthesesSniff::class);
};

View file

@ -0,0 +1,6 @@
parameters:
ignoreErrors:
-
message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#"
count: 1
path: src/Bootstrap.php

View file

@ -0,0 +1,7 @@
includes:
- phpstan-baseline.neon
parameters:
level: max
paths:
- src

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,3 @@
<?php declare(strict_types=1);
require __DIR__ . '/../src/Bootstrap.php';

View file

@ -0,0 +1,11 @@
<?php declare(strict_types=1);
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->paths([__DIR__ . '/src', __DIR__ . '/rector.php', __DIR__ . '/ecs.php']);
$rectorConfig->importNames();
$rectorConfig->sets([LevelSetList::UP_TO_PHP_81]);
};

View file

@ -0,0 +1,34 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework;
use Throwable;
use Whoops\Handler\PrettyPageHandler;
use Whoops\Run;
use function error_log;
use function error_reporting;
use function getenv;
use const E_ALL;
require __DIR__ . '/../vendor/autoload.php';
$environment = getenv('ENVIRONMENT') ?: 'dev';
error_reporting(E_ALL);
$whoops = new Run;
if ($environment === 'dev') {
$whoops->pushHandler(new PrettyPageHandler);
} else {
$whoops->pushHandler(function (Throwable $t) {
error_log('ERROR: ' . $t->getMessage(), $t->getCode());
echo 'Oooopsie';
});
}
$whoops->register();
echo 'Hello World!';

View file

@ -0,0 +1,46 @@
{
"name": "lubiana/no-framework",
"autoload": {
"psr-4": {
"Lubiana\\NoFramework\\": "src/"
}
},
"authors": [
{
"name": "example",
"email": "test@example.com"
}
],
"require": {
"php": ">=8.1",
"filp/whoops": "^2.14",
"laminas/laminas-diactoros": "^2.11"
},
"require-dev": {
"phpstan/phpstan": "^1.6",
"symfony/var-dumper": "^6.0",
"slevomat/coding-standard": "^7.2",
"symplify/easy-coding-standard": "^10.2",
"rector/rector": "^0.12.23",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-strict-rules": "^1.2",
"thecodingmachine/phpstan-strict-rules": "^1.0"
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"phpstan/extension-installer": true
}
},
"scripts": {
"serve": [
"Composer\\Config::disableProcessTimeout",
"php -S 0.0.0.0:1235 -t public"
],
"phpstan": "./vendor/bin/phpstan analyze",
"baseline": "./vendor/bin/phpstan analyze --generate-baseline",
"check": "./vendor/bin/ecs",
"fix": "./vendor/bin/ecs --fix",
"rector": "./vendor/bin/rector process"
}
}

1079
implementation/05/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

89
implementation/05/ecs.php Normal file
View file

@ -0,0 +1,89 @@
<?php declare(strict_types=1);
use PhpCsFixer\Fixer\Import\OrderedImportsFixer;
use PhpCsFixer\Fixer\Operator\NewWithBracesFixer;
use PhpCsFixer\Fixer\PhpTag\BlankLineAfterOpeningTagFixer;
use SlevomatCodingStandard\Sniffs\Classes\ClassConstantVisibilitySniff;
use SlevomatCodingStandard\Sniffs\ControlStructures\NewWithoutParenthesesSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\AlphabeticallySortedUsesSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\DisallowGroupUseSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\MultipleUsesPerLineSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\NamespaceSpacingSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\ReferenceUsedNamesOnlySniff;
use SlevomatCodingStandard\Sniffs\Namespaces\UseSpacingSniff;
use SlevomatCodingStandard\Sniffs\TypeHints\DeclareStrictTypesSniff;
use SlevomatCodingStandard\Sniffs\TypeHints\UnionTypeHintFormatSniff;
use Symplify\EasyCodingStandard\Config\ECSConfig;
use Symplify\EasyCodingStandard\ValueObject\Set\SetList;
return static function (ECSConfig $config): void {
$config->parallel();
$config->paths([__DIR__ . '/src', __DIR__ . '/ecs.php', __DIR__ . '/rector.php']);
$config->skip([BlankLineAfterOpeningTagFixer::class, OrderedImportsFixer::class, NewWithBracesFixer::class]);
$config->sets([
SetList::PSR_12,
SetList::STRICT,
SetList::ARRAY,
SetList::SPACES,
SetList::DOCBLOCK,
SetList::CLEAN_CODE,
SetList::COMMON,
SetList::COMMENTS,
SetList::NAMESPACES,
SetList::SYMPLIFY,
SetList::CONTROL_STRUCTURES,
]);
// force visibility declaration on class constants
$config->ruleWithConfiguration(ClassConstantVisibilitySniff::class, [
'fixable' => true,
]);
// sort all use statements
$config->rules([
AlphabeticallySortedUsesSniff::class,
DisallowGroupUseSniff::class,
MultipleUsesPerLineSniff::class,
NamespaceSpacingSniff::class,
]);
// import all namespaces, and event php core functions and classes
$config->ruleWithConfiguration(
ReferenceUsedNamesOnlySniff::class,
[
'allowFallbackGlobalConstants' => false,
'allowFallbackGlobalFunctions' => false,
'allowFullyQualifiedGlobalClasses' => false,
'allowFullyQualifiedGlobalConstants' => false,
'allowFullyQualifiedGlobalFunctions' => false,
'allowFullyQualifiedNameForCollidingClasses' => true,
'allowFullyQualifiedNameForCollidingConstants' => true,
'allowFullyQualifiedNameForCollidingFunctions' => true,
'searchAnnotations' => true,
]
);
// define newlines between use statements
$config->ruleWithConfiguration(UseSpacingSniff::class, [
'linesCountBeforeFirstUse' => 1,
'linesCountBetweenUseTypes' => 1,
'linesCountAfterLastUse' => 1,
]);
// strict types declaration should be on same line as opening tag
$config->ruleWithConfiguration(DeclareStrictTypesSniff::class, [
'declareOnFirstLine' => true,
'spacesCountAroundEqualsSign' => 0,
]);
// disallow ?Foo typehint in favor of Foo|null
$config->ruleWithConfiguration(UnionTypeHintFormatSniff::class, [
'withSpaces' => 'no',
'shortNullable' => 'no',
'nullPosition' => 'last',
]);
// Remove useless parentheses in new statements
$config->rule(NewWithoutParenthesesSniff::class);
};

View file

@ -0,0 +1,6 @@
parameters:
ignoreErrors:
-
message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#"
count: 1
path: src/Bootstrap.php

View file

@ -0,0 +1,7 @@
includes:
- phpstan-baseline.neon
parameters:
level: max
paths:
- src

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,3 @@
<?php declare(strict_types=1);
require __DIR__ . '/../src/Bootstrap.php';

View file

@ -0,0 +1,11 @@
<?php declare(strict_types=1);
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->paths([__DIR__ . '/src', __DIR__ . '/rector.php', __DIR__ . '/ecs.php']);
$rectorConfig->importNames();
$rectorConfig->sets([LevelSetList::UP_TO_PHP_81]);
};

View file

@ -0,0 +1,63 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\ServerRequestFactory;
use Throwable;
use Whoops\Handler\PrettyPageHandler;
use Whoops\Run;
use function error_log;
use function error_reporting;
use function getenv;
use function header;
use function sprintf;
use function strtolower;
use const E_ALL;
require __DIR__ . '/../vendor/autoload.php';
$environment = getenv('ENVIRONMENT') ?: 'dev';
error_reporting(E_ALL);
$whoops = new Run;
if ($environment === 'dev') {
$whoops->pushHandler(new PrettyPageHandler);
} else {
$whoops->pushHandler(function (Throwable $t) {
error_log('ERROR: ' . $t->getMessage(), $t->getCode());
echo 'Oooopsie';
});
}
$whoops->register();
$request = ServerRequestFactory::fromGlobals();
$response = new Response;
$response->getBody()
->write('Hello World! ');
$response->getBody()
->write('The Uri is: ' . $request->getUri()->getPath());
foreach ($response->getHeaders() as $name => $values) {
$first = strtolower($name) !== 'set-cookie';
foreach ($values as $value) {
$header = sprintf('%s: %s', $name, $value);
header($header, $first);
$first = false;
}
}
$statusLine = sprintf(
'HTTP/%s %s %s',
$response->getProtocolVersion(),
$response->getStatusCode(),
$response->getReasonPhrase()
);
header($statusLine, true, $response->getStatusCode());
echo $response->getBody();

View file

@ -0,0 +1,47 @@
{
"name": "lubiana/no-framework",
"autoload": {
"psr-4": {
"Lubiana\\NoFramework\\": "src/"
}
},
"authors": [
{
"name": "example",
"email": "test@example.com"
}
],
"require": {
"php": ">=8.1",
"filp/whoops": "^2.14",
"laminas/laminas-diactoros": "^2.11",
"nikic/fast-route": "^1.3"
},
"require-dev": {
"phpstan/phpstan": "^1.6",
"symfony/var-dumper": "^6.0",
"slevomat/coding-standard": "^7.2",
"symplify/easy-coding-standard": "^10.2",
"rector/rector": "^0.12.23",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-strict-rules": "^1.2",
"thecodingmachine/phpstan-strict-rules": "^1.0"
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"phpstan/extension-installer": true
}
},
"scripts": {
"serve": [
"Composer\\Config::disableProcessTimeout",
"php -S 0.0.0.0:1235 -t public"
],
"phpstan": "./vendor/bin/phpstan analyze",
"baseline": "./vendor/bin/phpstan analyze --generate-baseline",
"check": "./vendor/bin/ecs",
"fix": "./vendor/bin/ecs --fix",
"rector": "./vendor/bin/rector process"
}
}

1129
implementation/06/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,21 @@
<?php declare(strict_types=1);
use FastRoute\RouteCollector;
use Laminas\Diactoros\Response;
use Psr\Http\Message\ServerRequestInterface;
return function (RouteCollector $r) {
$r->addRoute('GET', '/hello[/{name}]', function (ServerRequestInterface $request) {
$name = $request->getAttribute('name', 'Stranger');
$response = (new Response)->withStatus(200);
$response->getBody()
->write('Hello ' . $name . '!');
return $response;
});
$r->addRoute('GET', '/other', function (ServerRequestInterface $request) {
$response = (new Response)->withStatus(200);
$response->getBody()
->write('This works too!');
return $response;
});
};

89
implementation/06/ecs.php Normal file
View file

@ -0,0 +1,89 @@
<?php declare(strict_types=1);
use PhpCsFixer\Fixer\Import\OrderedImportsFixer;
use PhpCsFixer\Fixer\Operator\NewWithBracesFixer;
use PhpCsFixer\Fixer\PhpTag\BlankLineAfterOpeningTagFixer;
use SlevomatCodingStandard\Sniffs\Classes\ClassConstantVisibilitySniff;
use SlevomatCodingStandard\Sniffs\ControlStructures\NewWithoutParenthesesSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\AlphabeticallySortedUsesSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\DisallowGroupUseSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\MultipleUsesPerLineSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\NamespaceSpacingSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\ReferenceUsedNamesOnlySniff;
use SlevomatCodingStandard\Sniffs\Namespaces\UseSpacingSniff;
use SlevomatCodingStandard\Sniffs\TypeHints\DeclareStrictTypesSniff;
use SlevomatCodingStandard\Sniffs\TypeHints\UnionTypeHintFormatSniff;
use Symplify\EasyCodingStandard\Config\ECSConfig;
use Symplify\EasyCodingStandard\ValueObject\Set\SetList;
return static function (ECSConfig $config): void {
$config->parallel();
$config->paths([__DIR__ . '/src', __DIR__ . '/config', __DIR__ . '/ecs.php', __DIR__ . '/rector.php']);
$config->skip([BlankLineAfterOpeningTagFixer::class, OrderedImportsFixer::class, NewWithBracesFixer::class]);
$config->sets([
SetList::PSR_12,
SetList::STRICT,
SetList::ARRAY,
SetList::SPACES,
SetList::DOCBLOCK,
SetList::CLEAN_CODE,
SetList::COMMON,
SetList::COMMENTS,
SetList::NAMESPACES,
SetList::SYMPLIFY,
SetList::CONTROL_STRUCTURES,
]);
// force visibility declaration on class constants
$config->ruleWithConfiguration(ClassConstantVisibilitySniff::class, [
'fixable' => true,
]);
// sort all use statements
$config->rules([
AlphabeticallySortedUsesSniff::class,
DisallowGroupUseSniff::class,
MultipleUsesPerLineSniff::class,
NamespaceSpacingSniff::class,
]);
// import all namespaces, and event php core functions and classes
$config->ruleWithConfiguration(
ReferenceUsedNamesOnlySniff::class,
[
'allowFallbackGlobalConstants' => false,
'allowFallbackGlobalFunctions' => false,
'allowFullyQualifiedGlobalClasses' => false,
'allowFullyQualifiedGlobalConstants' => false,
'allowFullyQualifiedGlobalFunctions' => false,
'allowFullyQualifiedNameForCollidingClasses' => true,
'allowFullyQualifiedNameForCollidingConstants' => true,
'allowFullyQualifiedNameForCollidingFunctions' => true,
'searchAnnotations' => true,
]
);
// define newlines between use statements
$config->ruleWithConfiguration(UseSpacingSniff::class, [
'linesCountBeforeFirstUse' => 1,
'linesCountBetweenUseTypes' => 1,
'linesCountAfterLastUse' => 1,
]);
// strict types declaration should be on same line as opening tag
$config->ruleWithConfiguration(DeclareStrictTypesSniff::class, [
'declareOnFirstLine' => true,
'spacesCountAroundEqualsSign' => 0,
]);
// disallow ?Foo typehint in favor of Foo|null
$config->ruleWithConfiguration(UnionTypeHintFormatSniff::class, [
'withSpaces' => 'no',
'shortNullable' => 'no',
'nullPosition' => 'last',
]);
// Remove useless parentheses in new statements
$config->rule(NewWithoutParenthesesSniff::class);
};

View file

@ -0,0 +1,6 @@
parameters:
ignoreErrors:
-
message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#"
count: 1
path: src/Bootstrap.php

View file

@ -0,0 +1,8 @@
includes:
- phpstan-baseline.neon
parameters:
level: max
paths:
- src
- config

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,3 @@
<?php declare(strict_types=1);
require __DIR__ . '/../src/Bootstrap.php';

View file

@ -0,0 +1,12 @@
<?php declare(strict_types=1);
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->paths([__DIR__ . '/src', __DIR__ . '/config', __DIR__ . '/ecs.php', __DIR__ . '/rector.php']);
$rectorConfig->importNames();
$rectorConfig->sets([LevelSetList::UP_TO_PHP_81]);
};

View file

@ -0,0 +1,97 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework;
use FastRoute\Dispatcher;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\ServerRequestFactory;
use Psr\Http\Message\ResponseInterface;
use Throwable;
use Whoops\Handler\PrettyPageHandler;
use Whoops\Run;
use function call_user_func;
use function error_log;
use function error_reporting;
use function FastRoute\simpleDispatcher;
use function getenv;
use function header;
use function sprintf;
use function strtolower;
use const E_ALL;
require __DIR__ . '/../vendor/autoload.php';
$environment = getenv('ENVIRONMENT') ?: 'dev';
error_reporting(E_ALL);
$whoops = new Run;
if ($environment === 'dev') {
$whoops->pushHandler(new PrettyPageHandler);
} else {
$whoops->pushHandler(function (Throwable $t) {
error_log('ERROR: ' . $t->getMessage(), $t->getCode());
echo 'Oooopsie';
});
}
$whoops->register();
$request = ServerRequestFactory::fromGlobals();
$response = new Response;
$response->getBody()
->write('Hello World! ');
$response->getBody()
->write('The Uri is: ' . $request->getUri()->getPath());
$routeDefinitionCallback = require __DIR__ . '/../config/routes.php';
$dispatcher = simpleDispatcher($routeDefinitionCallback);
$routeInfo = $dispatcher->dispatch($request->getMethod(), $request->getUri() ->getPath(),);
switch ($routeInfo[0]) {
case Dispatcher::METHOD_NOT_ALLOWED:
$response = (new Response)->withStatus(405);
$response->getBody()
->write('Method not allowed');
$response = $response->withStatus(405);
break;
case Dispatcher::FOUND:
$handler = $routeInfo[1];
foreach ($routeInfo[2] as $attributeName => $attributeValue) {
$request = $request->withAttribute($attributeName, $attributeValue);
}
/** @var ResponseInterface $response */
$response = call_user_func($handler, $request);
break;
case Dispatcher::NOT_FOUND:
default:
$response = (new Response)->withStatus(404);
$response->getBody()
->write('Not Found!');
break;
}
foreach ($response->getHeaders() as $name => $values) {
$first = strtolower($name) !== 'set-cookie';
foreach ($values as $value) {
$header = sprintf('%s: %s', $name, $value);
header($header, $first);
$first = false;
}
}
$statusLine = sprintf(
'HTTP/%s %s %s',
$response->getProtocolVersion(),
$response->getStatusCode(),
$response->getReasonPhrase()
);
header($statusLine, true, $response->getStatusCode());
echo $response->getBody();

View file

@ -0,0 +1,48 @@
{
"name": "lubiana/no-framework",
"autoload": {
"psr-4": {
"Lubiana\\NoFramework\\": "src/"
}
},
"authors": [
{
"name": "example",
"email": "test@example.com"
}
],
"require": {
"php": ">=8.1",
"filp/whoops": "^2.14",
"laminas/laminas-diactoros": "^2.11",
"nikic/fast-route": "^1.3",
"psr/http-server-handler": "^1.0"
},
"require-dev": {
"phpstan/phpstan": "^1.6",
"symfony/var-dumper": "^6.0",
"slevomat/coding-standard": "^7.2",
"symplify/easy-coding-standard": "^10.2",
"rector/rector": "^0.12.23",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-strict-rules": "^1.2",
"thecodingmachine/phpstan-strict-rules": "^1.0"
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"phpstan/extension-installer": true
}
},
"scripts": {
"serve": [
"Composer\\Config::disableProcessTimeout",
"php -S 0.0.0.0:1235 -t public"
],
"phpstan": "./vendor/bin/phpstan analyze",
"baseline": "./vendor/bin/phpstan analyze --generate-baseline",
"check": "./vendor/bin/ecs",
"fix": "./vendor/bin/ecs --fix",
"rector": "./vendor/bin/rector process"
}
}

1186
implementation/07/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
use FastRoute\RouteCollector;
use Lubian\NoFramework\Action\Hello;
use Lubian\NoFramework\Action\Other;
return function (RouteCollector $r) {
$r->addRoute('GET', '/hello[/{name}]', Hello::class);
$r->addRoute('GET', '/other', Other::class);
};

89
implementation/07/ecs.php Normal file
View file

@ -0,0 +1,89 @@
<?php declare(strict_types=1);
use PhpCsFixer\Fixer\Import\OrderedImportsFixer;
use PhpCsFixer\Fixer\Operator\NewWithBracesFixer;
use PhpCsFixer\Fixer\PhpTag\BlankLineAfterOpeningTagFixer;
use SlevomatCodingStandard\Sniffs\Classes\ClassConstantVisibilitySniff;
use SlevomatCodingStandard\Sniffs\ControlStructures\NewWithoutParenthesesSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\AlphabeticallySortedUsesSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\DisallowGroupUseSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\MultipleUsesPerLineSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\NamespaceSpacingSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\ReferenceUsedNamesOnlySniff;
use SlevomatCodingStandard\Sniffs\Namespaces\UseSpacingSniff;
use SlevomatCodingStandard\Sniffs\TypeHints\DeclareStrictTypesSniff;
use SlevomatCodingStandard\Sniffs\TypeHints\UnionTypeHintFormatSniff;
use Symplify\EasyCodingStandard\Config\ECSConfig;
use Symplify\EasyCodingStandard\ValueObject\Set\SetList;
return static function (ECSConfig $config): void {
$config->parallel();
$config->paths([__DIR__ . '/src', __DIR__ . '/config', __DIR__ . '/ecs.php', __DIR__ . '/rector.php']);
$config->skip([BlankLineAfterOpeningTagFixer::class, OrderedImportsFixer::class, NewWithBracesFixer::class]);
$config->sets([
SetList::PSR_12,
SetList::STRICT,
SetList::ARRAY,
SetList::SPACES,
SetList::DOCBLOCK,
SetList::CLEAN_CODE,
SetList::COMMON,
SetList::COMMENTS,
SetList::NAMESPACES,
SetList::SYMPLIFY,
SetList::CONTROL_STRUCTURES,
]);
// force visibility declaration on class constants
$config->ruleWithConfiguration(ClassConstantVisibilitySniff::class, [
'fixable' => true,
]);
// sort all use statements
$config->rules([
AlphabeticallySortedUsesSniff::class,
DisallowGroupUseSniff::class,
MultipleUsesPerLineSniff::class,
NamespaceSpacingSniff::class,
]);
// import all namespaces, and event php core functions and classes
$config->ruleWithConfiguration(
ReferenceUsedNamesOnlySniff::class,
[
'allowFallbackGlobalConstants' => false,
'allowFallbackGlobalFunctions' => false,
'allowFullyQualifiedGlobalClasses' => false,
'allowFullyQualifiedGlobalConstants' => false,
'allowFullyQualifiedGlobalFunctions' => false,
'allowFullyQualifiedNameForCollidingClasses' => true,
'allowFullyQualifiedNameForCollidingConstants' => true,
'allowFullyQualifiedNameForCollidingFunctions' => true,
'searchAnnotations' => true,
]
);
// define newlines between use statements
$config->ruleWithConfiguration(UseSpacingSniff::class, [
'linesCountBeforeFirstUse' => 1,
'linesCountBetweenUseTypes' => 1,
'linesCountAfterLastUse' => 1,
]);
// strict types declaration should be on same line as opening tag
$config->ruleWithConfiguration(DeclareStrictTypesSniff::class, [
'declareOnFirstLine' => true,
'spacesCountAroundEqualsSign' => 0,
]);
// disallow ?Foo typehint in favor of Foo|null
$config->ruleWithConfiguration(UnionTypeHintFormatSniff::class, [
'withSpaces' => 'no',
'shortNullable' => 'no',
'nullPosition' => 'last',
]);
// Remove useless parentheses in new statements
$config->rule(NewWithoutParenthesesSniff::class);
};

View file

@ -0,0 +1,6 @@
parameters:
ignoreErrors:
-
message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#"
count: 1
path: src/Bootstrap.php

View file

@ -0,0 +1,8 @@
includes:
- phpstan-baseline.neon
parameters:
level: max
paths:
- src
- config

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,3 @@
<?php declare(strict_types=1);
require __DIR__ . '/../src/Bootstrap.php';

View file

@ -0,0 +1,12 @@
<?php declare(strict_types=1);
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->paths([__DIR__ . '/src', __DIR__ . '/config', __DIR__ . '/ecs.php', __DIR__ . '/rector.php']);
$rectorConfig->importNames();
$rectorConfig->sets([LevelSetList::UP_TO_PHP_81]);
};

View file

@ -0,0 +1,20 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Action;
use Laminas\Diactoros\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class Hello implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
$name = $request->getAttribute('name', 'Stranger');
$response = (new Response)->withStatus(200);
$response->getBody()
->write('Hello ' . $name . '!');
return $response;
}
}

View file

@ -0,0 +1,19 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Action;
use Laminas\Diactoros\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class Other implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
$response = (new Response)->withStatus(200);
$response->getBody()
->write('This works too!');
return $response;
}
}

View file

@ -0,0 +1,101 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework;
use FastRoute\Dispatcher;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\ServerRequestFactory;
use Lubian\NoFramework\Exception\InternalServerError;
use Lubian\NoFramework\Exception\MethodNotAllowed;
use Lubian\NoFramework\Exception\NotFound;
use Psr\Http\Server\RequestHandlerInterface;
use Throwable;
use Whoops\Handler\PrettyPageHandler;
use Whoops\Run;
use function assert;
use function error_log;
use function error_reporting;
use function FastRoute\simpleDispatcher;
use function getenv;
use function header;
use function sprintf;
use function strtolower;
use const E_ALL;
require __DIR__ . '/../vendor/autoload.php';
$environment = getenv('ENVIRONMENT') ?: 'dev';
error_reporting(E_ALL);
$whoops = new Run;
if ($environment === 'dev') {
$whoops->pushHandler(new PrettyPageHandler);
} else {
$whoops->pushHandler(function (Throwable $t) {
error_log('ERROR: ' . $t->getMessage(), $t->getCode());
echo 'Oooopsie';
});
}
$whoops->register();
$request = ServerRequestFactory::fromGlobals();
$routeDefinitionCallback = require __DIR__ . '/../config/routes.php';
$dispatcher = simpleDispatcher($routeDefinitionCallback);
$routeInfo = $dispatcher->dispatch($request->getMethod(), $request->getUri() ->getPath(),);
try {
switch ($routeInfo[0]) {
case Dispatcher::FOUND:
$className = $routeInfo[1];
$handler = new $className;
assert($handler instanceof RequestHandlerInterface);
foreach ($routeInfo[2] as $attributeName => $attributeValue) {
$request = $request->withAttribute($attributeName, $attributeValue);
}
$response = $handler->handle($request);
break;
case Dispatcher::METHOD_NOT_ALLOWED:
throw new MethodNotAllowed;
case Dispatcher::NOT_FOUND:
default:
throw new NotFound;
}
} catch (MethodNotAllowed) {
$response = (new Response)->withStatus(405);
$response->getBody()
->write('Method not Allowed');
} catch (NotFound) {
$response = (new Response)->withStatus(404);
$response->getBody()
->write('Not Found');
} catch (Throwable $t) {
throw new InternalServerError($t->getMessage(), $t->getCode(), $t);
}
foreach ($response->getHeaders() as $name => $values) {
$first = strtolower($name) !== 'set-cookie';
foreach ($values as $value) {
$header = sprintf('%s: %s', $name, $value);
header($header, $first);
$first = false;
}
}
$statusLine = sprintf(
'HTTP/%s %s %s',
$response->getProtocolVersion(),
$response->getStatusCode(),
$response->getReasonPhrase()
);
header($statusLine, true, $response->getStatusCode());
echo $response->getBody();

View file

@ -0,0 +1,9 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Exception;
use Exception;
final class InternalServerError extends Exception
{
}

View file

@ -0,0 +1,9 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Exception;
use Exception;
final class MethodNotAllowed extends Exception
{
}

View file

@ -0,0 +1,9 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Exception;
use Exception;
final class NotFound extends Exception
{
}

View file

@ -0,0 +1,48 @@
{
"name": "lubian/no-framework",
"autoload": {
"psr-4": {
"Lubian\\NoFramework\\": "src/"
}
},
"authors": [
{
"name": "example",
"email": "test@example.com"
}
],
"require": {
"php": ">=8.1",
"filp/whoops": "^2.14",
"laminas/laminas-diactoros": "^2.11",
"nikic/fast-route": "^1.3",
"psr/http-server-handler": "^1.0"
},
"require-dev": {
"phpstan/phpstan": "^1.6",
"symfony/var-dumper": "^6.0",
"slevomat/coding-standard": "^7.2",
"symplify/easy-coding-standard": "^10.2",
"rector/rector": "^0.12.23",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-strict-rules": "^1.2",
"thecodingmachine/phpstan-strict-rules": "^1.0"
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"phpstan/extension-installer": true
}
},
"scripts": {
"serve": [
"Composer\\Config::disableProcessTimeout",
"php -S 0.0.0.0:1235 -t public"
],
"phpstan": "./vendor/bin/phpstan analyze",
"baseline": "./vendor/bin/phpstan analyze --generate-baseline",
"check": "./vendor/bin/ecs",
"fix": "./vendor/bin/ecs --fix",
"rector": "./vendor/bin/rector process"
}
}

1186
implementation/08/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
use FastRoute\RouteCollector;
use Lubian\NoFramework\Action\Hello;
use Lubian\NoFramework\Action\Other;
return function (RouteCollector $r) {
$r->addRoute('GET', '/hello[/{name}]', Hello::class);
$r->addRoute('GET', '/other', Other::class);
};

89
implementation/08/ecs.php Normal file
View file

@ -0,0 +1,89 @@
<?php declare(strict_types=1);
use PhpCsFixer\Fixer\Import\OrderedImportsFixer;
use PhpCsFixer\Fixer\Operator\NewWithBracesFixer;
use PhpCsFixer\Fixer\PhpTag\BlankLineAfterOpeningTagFixer;
use SlevomatCodingStandard\Sniffs\Classes\ClassConstantVisibilitySniff;
use SlevomatCodingStandard\Sniffs\ControlStructures\NewWithoutParenthesesSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\AlphabeticallySortedUsesSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\DisallowGroupUseSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\MultipleUsesPerLineSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\NamespaceSpacingSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\ReferenceUsedNamesOnlySniff;
use SlevomatCodingStandard\Sniffs\Namespaces\UseSpacingSniff;
use SlevomatCodingStandard\Sniffs\TypeHints\DeclareStrictTypesSniff;
use SlevomatCodingStandard\Sniffs\TypeHints\UnionTypeHintFormatSniff;
use Symplify\EasyCodingStandard\Config\ECSConfig;
use Symplify\EasyCodingStandard\ValueObject\Set\SetList;
return static function (ECSConfig $config): void {
$config->parallel();
$config->paths([__DIR__ . '/src', __DIR__ . '/config', __DIR__ . '/ecs.php', __DIR__ . '/rector.php']);
$config->skip([BlankLineAfterOpeningTagFixer::class, OrderedImportsFixer::class, NewWithBracesFixer::class]);
$config->sets([
SetList::PSR_12,
SetList::STRICT,
SetList::ARRAY,
SetList::SPACES,
SetList::DOCBLOCK,
SetList::CLEAN_CODE,
SetList::COMMON,
SetList::COMMENTS,
SetList::NAMESPACES,
SetList::SYMPLIFY,
SetList::CONTROL_STRUCTURES,
]);
// force visibility declaration on class constants
$config->ruleWithConfiguration(ClassConstantVisibilitySniff::class, [
'fixable' => true,
]);
// sort all use statements
$config->rules([
AlphabeticallySortedUsesSniff::class,
DisallowGroupUseSniff::class,
MultipleUsesPerLineSniff::class,
NamespaceSpacingSniff::class,
]);
// import all namespaces, and event php core functions and classes
$config->ruleWithConfiguration(
ReferenceUsedNamesOnlySniff::class,
[
'allowFallbackGlobalConstants' => false,
'allowFallbackGlobalFunctions' => false,
'allowFullyQualifiedGlobalClasses' => false,
'allowFullyQualifiedGlobalConstants' => false,
'allowFullyQualifiedGlobalFunctions' => false,
'allowFullyQualifiedNameForCollidingClasses' => true,
'allowFullyQualifiedNameForCollidingConstants' => true,
'allowFullyQualifiedNameForCollidingFunctions' => true,
'searchAnnotations' => true,
]
);
// define newlines between use statements
$config->ruleWithConfiguration(UseSpacingSniff::class, [
'linesCountBeforeFirstUse' => 1,
'linesCountBetweenUseTypes' => 1,
'linesCountAfterLastUse' => 1,
]);
// strict types declaration should be on same line as opening tag
$config->ruleWithConfiguration(DeclareStrictTypesSniff::class, [
'declareOnFirstLine' => true,
'spacesCountAroundEqualsSign' => 0,
]);
// disallow ?Foo typehint in favor of Foo|null
$config->ruleWithConfiguration(UnionTypeHintFormatSniff::class, [
'withSpaces' => 'no',
'shortNullable' => 'no',
'nullPosition' => 'last',
]);
// Remove useless parentheses in new statements
$config->rule(NewWithoutParenthesesSniff::class);
};

View file

@ -0,0 +1,6 @@
parameters:
ignoreErrors:
-
message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#"
count: 1
path: src/Bootstrap.php

View file

@ -0,0 +1,8 @@
includes:
- phpstan-baseline.neon
parameters:
level: max
paths:
- src
- config

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,3 @@
<?php declare(strict_types=1);
require __DIR__ . '/../src/Bootstrap.php';

View file

@ -0,0 +1,12 @@
<?php declare(strict_types=1);
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->paths([__DIR__ . '/src', __DIR__ . '/config', __DIR__ . '/ecs.php', __DIR__ . '/rector.php']);
$rectorConfig->importNames();
$rectorConfig->sets([LevelSetList::UP_TO_PHP_81]);
};

View file

@ -0,0 +1,25 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Action;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class Hello implements RequestHandlerInterface
{
public function __construct(private readonly ResponseInterface $response)
{
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
$name = $request->getAttribute('name', 'Stranger');
$body = $this->response->getBody();
$body->write('Hello ' . $name . '!');
return $this->response->withBody($body)
->withStatus(200);
}
}

View file

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Action;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class Other implements RequestHandlerInterface
{
public function __construct(private readonly ResponseInterface $response)
{
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
$body = $this->response->getBody();
$body->write('This works too!');
return $this->response->withBody($body)
->withStatus(200);
}
}

View file

@ -0,0 +1,101 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework;
use FastRoute\Dispatcher;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\ServerRequestFactory;
use Lubian\NoFramework\Exception\InternalServerError;
use Lubian\NoFramework\Exception\MethodNotAllowed;
use Lubian\NoFramework\Exception\NotFound;
use Psr\Http\Server\RequestHandlerInterface;
use Throwable;
use Whoops\Handler\PrettyPageHandler;
use Whoops\Run;
use function assert;
use function error_log;
use function error_reporting;
use function FastRoute\simpleDispatcher;
use function getenv;
use function header;
use function sprintf;
use function strtolower;
use const E_ALL;
require __DIR__ . '/../vendor/autoload.php';
$environment = getenv('ENVIRONMENT') ?: 'dev';
error_reporting(E_ALL);
$whoops = new Run;
if ($environment === 'dev') {
$whoops->pushHandler(new PrettyPageHandler);
} else {
$whoops->pushHandler(function (Throwable $t) {
error_log('ERROR: ' . $t->getMessage(), $t->getCode());
echo 'Oooopsie';
});
}
$whoops->register();
$request = ServerRequestFactory::fromGlobals();
$response = new Response;
$routeDefinitionCallback = require __DIR__ . '/../config/routes.php';
$dispatcher = simpleDispatcher($routeDefinitionCallback);
$routeInfo = $dispatcher->dispatch($request->getMethod(), $request->getUri() ->getPath(),);
try {
switch ($routeInfo[0]) {
case Dispatcher::FOUND:
$className = $routeInfo[1];
$handler = new $className($response);
assert($handler instanceof RequestHandlerInterface);
foreach ($routeInfo[2] as $attributeName => $attributeValue) {
$request = $request->withAttribute($attributeName, $attributeValue);
}
$response = $handler->handle($request);
break;
case Dispatcher::METHOD_NOT_ALLOWED:
throw new MethodNotAllowed;
case Dispatcher::NOT_FOUND:
default:
throw new NotFound;
}
} catch (MethodNotAllowed) {
$response = (new Response)->withStatus(405);
$response->getBody()
->write('Method not Allowed');
} catch (NotFound) {
$response = (new Response)->withStatus(404);
$response->getBody()
->write('Not Found');
} catch (Throwable $t) {
throw new InternalServerError($t->getMessage(), $t->getCode(), $t);
}
foreach ($response->getHeaders() as $name => $values) {
$first = strtolower($name) !== 'set-cookie';
foreach ($values as $value) {
$header = sprintf('%s: %s', $name, $value);
header($header, $first);
$first = false;
}
}
$statusLine = sprintf(
'HTTP/%s %s %s',
$response->getProtocolVersion(),
$response->getStatusCode(),
$response->getReasonPhrase()
);
header($statusLine, true, $response->getStatusCode());
echo $response->getBody();

View file

@ -0,0 +1,9 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Exception;
use Exception;
final class InternalServerError extends Exception
{
}

View file

@ -0,0 +1,9 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Exception;
use Exception;
final class MethodNotAllowed extends Exception
{
}

View file

@ -0,0 +1,9 @@
<?php declare(strict_types=1);
namespace Lubian\NoFramework\Exception;
use Exception;
final class NotFound extends Exception
{
}

View file

@ -0,0 +1,50 @@
{
"name": "lubian/no-framework",
"autoload": {
"psr-4": {
"Lubian\\NoFramework\\": "src/"
}
},
"authors": [
{
"name": "example",
"email": "test@example.com"
}
],
"require": {
"php": ">=8.1",
"filp/whoops": "^2.14",
"laminas/laminas-diactoros": "^2.11",
"nikic/fast-route": "^1.3",
"psr/http-server-handler": "^1.0",
"psr/container": "^1.0",
"php-di/php-di": "^6.4"
},
"require-dev": {
"phpstan/phpstan": "^1.6",
"symfony/var-dumper": "^6.0",
"slevomat/coding-standard": "^7.2",
"symplify/easy-coding-standard": "^10.2",
"rector/rector": "^0.12.23",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-strict-rules": "^1.2",
"thecodingmachine/phpstan-strict-rules": "^1.0"
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"phpstan/extension-installer": true
}
},
"scripts": {
"serve": [
"Composer\\Config::disableProcessTimeout",
"php -S 0.0.0.0:1235 -t public"
],
"phpstan": "./vendor/bin/phpstan analyze",
"baseline": "./vendor/bin/phpstan analyze --generate-baseline",
"check": "./vendor/bin/ecs",
"fix": "./vendor/bin/ecs --fix",
"rector": "./vendor/bin/rector process"
}
}

1466
implementation/09/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,23 @@
<?php declare(strict_types=1);
use DI\ContainerBuilder;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\ServerRequestFactory;
use Lubian\NoFramework\Service\Time\Clock;
use Lubian\NoFramework\Service\Time\SystemClock;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use function FastRoute\simpleDispatcher;
$builder = new ContainerBuilder;
$builder->addDefinitions(
[
ServerRequestInterface::class => fn () => ServerRequestFactory::fromGlobals(),
ResponseInterface::class => fn () => new Response,
FastRoute\Dispatcher::class => fn () => simpleDispatcher(require __DIR__ . '/routes.php'),
Clock::class => fn () => new SystemClock,
]
);
return $builder->build();

View file

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
use FastRoute\RouteCollector;
use Lubian\NoFramework\Action\Hello;
use Lubian\NoFramework\Action\Other;
return function (RouteCollector $r) {
$r->addRoute('GET', '/hello[/{name}]', Hello::class);
$r->addRoute('GET', '/other', Other::class);
};

89
implementation/09/ecs.php Normal file
View file

@ -0,0 +1,89 @@
<?php declare(strict_types=1);
use PhpCsFixer\Fixer\Import\OrderedImportsFixer;
use PhpCsFixer\Fixer\Operator\NewWithBracesFixer;
use PhpCsFixer\Fixer\PhpTag\BlankLineAfterOpeningTagFixer;
use SlevomatCodingStandard\Sniffs\Classes\ClassConstantVisibilitySniff;
use SlevomatCodingStandard\Sniffs\ControlStructures\NewWithoutParenthesesSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\AlphabeticallySortedUsesSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\DisallowGroupUseSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\MultipleUsesPerLineSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\NamespaceSpacingSniff;
use SlevomatCodingStandard\Sniffs\Namespaces\ReferenceUsedNamesOnlySniff;
use SlevomatCodingStandard\Sniffs\Namespaces\UseSpacingSniff;
use SlevomatCodingStandard\Sniffs\TypeHints\DeclareStrictTypesSniff;
use SlevomatCodingStandard\Sniffs\TypeHints\UnionTypeHintFormatSniff;
use Symplify\EasyCodingStandard\Config\ECSConfig;
use Symplify\EasyCodingStandard\ValueObject\Set\SetList;
return static function (ECSConfig $config): void {
$config->parallel();
$config->paths([__DIR__ . '/src', __DIR__ . '/config', __DIR__ . '/ecs.php', __DIR__ . '/rector.php']);
$config->skip([BlankLineAfterOpeningTagFixer::class, OrderedImportsFixer::class, NewWithBracesFixer::class]);
$config->sets([
SetList::PSR_12,
SetList::STRICT,
SetList::ARRAY,
SetList::SPACES,
SetList::DOCBLOCK,
SetList::CLEAN_CODE,
SetList::COMMON,
SetList::COMMENTS,
SetList::NAMESPACES,
SetList::SYMPLIFY,
SetList::CONTROL_STRUCTURES,
]);
// force visibility declaration on class constants
$config->ruleWithConfiguration(ClassConstantVisibilitySniff::class, [
'fixable' => true,
]);
// sort all use statements
$config->rules([
AlphabeticallySortedUsesSniff::class,
DisallowGroupUseSniff::class,
MultipleUsesPerLineSniff::class,
NamespaceSpacingSniff::class,
]);
// import all namespaces, and event php core functions and classes
$config->ruleWithConfiguration(
ReferenceUsedNamesOnlySniff::class,
[
'allowFallbackGlobalConstants' => false,
'allowFallbackGlobalFunctions' => false,
'allowFullyQualifiedGlobalClasses' => false,
'allowFullyQualifiedGlobalConstants' => false,
'allowFullyQualifiedGlobalFunctions' => false,
'allowFullyQualifiedNameForCollidingClasses' => true,
'allowFullyQualifiedNameForCollidingConstants' => true,
'allowFullyQualifiedNameForCollidingFunctions' => true,
'searchAnnotations' => true,
]
);
// define newlines between use statements
$config->ruleWithConfiguration(UseSpacingSniff::class, [
'linesCountBeforeFirstUse' => 1,
'linesCountBetweenUseTypes' => 1,
'linesCountAfterLastUse' => 1,
]);
// strict types declaration should be on same line as opening tag
$config->ruleWithConfiguration(DeclareStrictTypesSniff::class, [
'declareOnFirstLine' => true,
'spacesCountAroundEqualsSign' => 0,
]);
// disallow ?Foo typehint in favor of Foo|null
$config->ruleWithConfiguration(UnionTypeHintFormatSniff::class, [
'withSpaces' => 'no',
'shortNullable' => 'no',
'nullPosition' => 'last',
]);
// Remove useless parentheses in new statements
$config->rule(NewWithoutParenthesesSniff::class);
};

View file

@ -0,0 +1,6 @@
parameters:
ignoreErrors:
-
message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#"
count: 1
path: src/Bootstrap.php

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