└── Readme.md /Readme.md: -------------------------------------------------------------------------------- 1 | # Workshop: Symfony internals 2 | 3 | This workshop slowly builds a framework. 4 | 5 | This workshop requires you to be able to run standard Symfony 4 application. We use PHP 7.1+. 6 | 7 | Here is the slides for the presentation: https://www.slideshare.net/TobiasNyholm/symfony-internals-workshop 8 | 9 | ## Part 1 10 | 11 | Below are a short description of each exercise. When you completed one exercise 12 | and starting with the next one. You can continue with your code or "restart" from 13 | the branch mentioned in the exercise description. 14 | 15 | ### Exercise 1: HTTP objects 16 | 17 | Branch: [1-jbof](/../../tree/1-jbof) 18 | 19 | We start of with *jbof*, Just a bunch of files. Nothing will be faster than this. 20 | This is the fastest "framework" you can get. 21 | 22 | But this is not really scalable. Lets start with adding PSR-7 requests and responses. 23 | If you do not have any favorite PSR-7 implementation you could download `nyholm/psr7`. 24 | 25 | You can test your application with: 26 | 27 | ``` 28 | php -S 127.0.0.1:8080 29 | ``` 30 | 31 | ### Exercise 2: Controller 32 | 33 | Branch: [2-request](/../../tree/2-request) 34 | 35 | Let's move "our" code out from index.php. Crete a "controller" class with a function 36 | that takes a request and returns a response. You should put your controllers under 37 | `src/` (maybe you want to create more subfolders). 38 | 39 | It is a good practise to have your frontend controller (index.php) in a subdirectory. 40 | That means the webserver do not have access to the root of you application and can 41 | directly access any file. Lets put index.php in a `public/` directory. 42 | 43 | 44 | You can test your application with: 45 | 46 | ``` 47 | php -S 127.0.0.1:8080 -t public 48 | ``` 49 | 50 | ### Exercise 3: Event loop 51 | 52 | Branch: [3-controller](/../../tree/3-controller) 53 | 54 | Excellent. The framework looks pretty good now. But we do not like how we are doing 55 | the routing in index.php. It would mean that we need to edit index.php every time 56 | we want to add a new controller. 57 | 58 | Let's implement an event loop. Run `composer require "relay/relay:1.1"`. Have a quick 59 | look at [the documentation](http://relayphp.com/) and then create a `RouterMiddleware` 60 | that implements the `MiddlewareInterface` as follows: 61 | 62 | ```php 63 | 64 | namespace App\Middleware; 65 | 66 | use Psr\Http\Message\ResponseInterface; 67 | use Psr\Http\Message\ServerRequestInterface; 68 | 69 | interface MiddlewareInterface 70 | { 71 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next); 72 | } 73 | ``` 74 | 75 | Make sure to build and run your array of middleware in index.php. 76 | 77 | **Note:** As an alternative to `relay/relay` you can use this simple [Runner.php](https://gist.github.com/Nyholm/a7166e6e570738bee75612c3608aa4e3). 78 | 79 | ### Exercise 4: Cache 80 | 81 | Branch: [4-event-loop](/../../tree/4-event-loop) 82 | 83 | In this exercise we are going to add a cache system. Use your favorite cache library. 84 | If you do not got a favorite, [php-cache](http://www.php-cache.com/en/latest/) is a 85 | good one. (`composer require cache/filesystem-adapter`). 86 | 87 | Create a middleware that cache the requests. So the controller is not hit twice with the same URL. 88 | 89 | ### Exercise 5: Container 90 | 91 | Branch: [5-cache](/../../tree/5-cache) 92 | 93 | This is awesome. Our really fast framework is now even faster. But it is hard to 94 | update the HTML of your controller in development since the response is cached. 95 | 96 | We want to introduce the concept of "environment" to enable the cache feature 97 | only in "prod". In "dev" environment we want to use a null cache like `cache/void-adapter`. 98 | 99 | Install `symfony/dependency-injection` and create `src/Kernel.php` that should be 100 | responsible for building the container and building the middleware array. A good 101 | idea is to only have one public function: `Kernel::handle(RequestInterface $request): ResponseInterface`. 102 | 103 | The `Kernel` should be responsible for "building" a container which all our services 104 | are loaded. (See [documentation](https://symfony.com/doc/current/components/dependency_injection.html#setting-up-the-container-with-configuration-files)) 105 | After the container is built you should run `ContainerBuilder::compile()` before you 106 | use the container. 107 | 108 | **Hint:** To *emit* (send) the response: https://github.com/Nyholm/psr7#emitting-a-response 109 | 110 | #### Bonus exercise 111 | 112 | For performance reasons, we should not build the container at every request in production environment. 113 | We should used a cached/dumped container. See the [Symfony documentation](https://symfony.com/doc/current/components/dependency_injection/compilation.html#dumping-the-configuration-for-performance) 114 | about how to dump a container. 115 | 116 | ### Exercise 6: Security 117 | 118 | Branch: [6-container](/../../tree/6-container) 119 | 120 | Looking better and better now. We want to have that "admin" stuff we had in **jbof**. 121 | Lets add some security. Lets first separate *Authentication* (Who are you?) from 122 | *Authorization* (What are you allowed to do?). These are two new *things* so we 123 | need new middleware. 124 | 125 | For this exercise, make sure to create an admin controller that only the user 126 | "alice" can see. 127 | You do not need to validate passwords. 128 | 129 | **Note:** If you return with a response like: 130 | ```php 131 | return new Response(401, ['WWW-Authenticate'=>'Basic realm="Admin area"'], 'This page is protected'); 132 | ``` 133 | A HTTP Basic authentication login window will show for the user. Read the input to that window by: 134 | 135 | ```php 136 | $auth = $request->getServerParams()['PHP_AUTH_USER'] ?? ''; 137 | $pass = $request->getServerParams()['PHP_AUTH_PW'] ?? ''; 138 | ``` 139 | 140 | ### Exercise 7: Toolbar 141 | 142 | Branch: [7-security](/../../tree/7-security) 143 | 144 | When in "dev" environment, it is nice to have a toolbar that shows some statistics 145 | about the request. Lets try to implement that. Since this is a new feature we need 146 | a new middleware. 147 | 148 | The toolbar should be added just before `` of the response. 149 | 150 | **Note:** To gather statistics from ie the cache service you need to create a decorator 151 | that [decorates the service](https://symfony.com/doc/current/service_container/service_decoration.html). 152 | 153 | ```php 154 | class CacheDataCollector implements CacheItemPoolInterface 155 | { 156 | private $real; 157 | private $calls; 158 | 159 | public function __construct(CacheItemPoolInterface $cache) 160 | { 161 | $this->real = $cache; 162 | } 163 | 164 | public function getCalls() 165 | { 166 | return $this->calls; 167 | } 168 | 169 | public function getItem($key) 170 | { 171 | $this->calls['getItem'][] = ['key'=>$key]; 172 | return $this->real->getItem($key); 173 | } 174 | // ... 175 | ``` 176 | 177 | 178 | ### Exercise 8: Exception 179 | 180 | Branch: [8-toolbar](/../../tree/8-toolbar) 181 | 182 | Lets create this controller: 183 | 184 | ```php 185 | use Psr\Http\Message\RequestInterface; 186 | 187 | class ExceptionController 188 | { 189 | public function run(RequestInterface $request) 190 | { 191 | throw new \RuntimeException('This is an exception'); 192 | } 193 | } 194 | ``` 195 | 196 | We want to print a helpful message in "dev" environment and a pretty "I'm sorry" 197 | page in "prod". Since this is a new feature we need a new middleware. 198 | 199 | ## Part 2 200 | 201 | We built a real good framework now. It is a simple Symfony. Since we like the 202 | Symfony ecosystem so much. Lets try to refactor our framework to use more Symfony 203 | components. 204 | 205 | 206 | ### Exercise 1: Use Autowiring 207 | 208 | Branch: [9-exception](/../../tree/9-exception) 209 | 210 | (Only do this if you got time and energy. This exercise could easily be skipped.) 211 | 212 | We love autowiring. It makes our service configuration small and nice. Try to 213 | autowire as many services as you can. The [Symfony documentation](https://symfony.com/doc/current/service_container.html) 214 | may be a good reference. 215 | 216 | ### Exercise 2: Add CLI 217 | 218 | Branch: [21-autowire](/../../tree/21-autowire) 219 | 220 | The Command Line Interface is just another frontend controller. The code is similar 221 | to our index.php. You should require `symfony/console` and create a `./bin/console` 222 | file. 223 | 224 | You should also create a small command class to test your `./bin/console`. 225 | 226 | #### Bonus exercise 227 | 228 | Make sure you can register your command in the service container. This allows 229 | command classes to use dependency injection as normal. 230 | 231 | **Hint:** There is a class `Symfony\Component\Console\CommandLoader\ContainerCommandLoader` 232 | that is registered With the `AddConsoleCommandPass`. 233 | 234 | ```php 235 | use Symfony\Component\Console\Command\Command; 236 | use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass; 237 | 238 | $container->registerForAutoconfiguration(Command::class)->addTag('console.command'); 239 | $container->addCompilerPass(new AddConsoleCommandPass()); 240 | ``` 241 | 242 | ### Exercise 3: Event Dispatcher 243 | 244 | Branch: [22-command](/../../tree/22-command) 245 | 246 | The time has come for us to replace the heart of our application, the event loop. 247 | This is a major refactoring and we need to update all our middleware. Start by 248 | downloadning `symfony/event-dispatcher`. Instead of having one "loop" as we did 249 | with `relay/relay` we can now create multiple loops (or events). 250 | 251 | Create event classes for: 252 | * An incoming request. 253 | * Parsing/Filtering of a response 254 | * Exception 255 | 256 | Our middlewares should be refactored to `EventSubscribers`. The `Kernel::handle` 257 | is also subject for a rewrite. 258 | 259 | The benefit of different "loops" (or events) is that we can show the toolbar on 260 | an exception page. That was not possile before. 261 | 262 | **Hint:** One feature of `symfony/event-dispatcher` is that we can use 263 | `$event->stopPropagation()` which stops the current loop. That could be 264 | useful in our cache or security middleware. 265 | 266 | ### Exercise 4: HTTP Foundation 267 | 268 | Branch: [23-event-dispatcher](/../../tree/23-event-dispatcher) 269 | 270 | To fully be able to integrate with symfony components we should be using Symfonys 271 | implementation of request and responses. So lets remove PSR-7 and use `symfony/http-foundation`. 272 | 273 | It is a lot to rewrite but it is just simple changes. Feel free to skip ahead 274 | to next exercise. 275 | 276 | ### Exercise 5: Cache 277 | 278 | Branch: [24-http-foundation](/../../tree/24-http-foundation) 279 | 280 | PHP-cache is great. But we are moving towards full Symfony. Lets use 281 | `symfony/cache` instead. 282 | 283 | **Note:** This is a simple change becuase PSR-6 is awesome. 284 | 285 | ### Exercise 6: HTTP Kernel 286 | 287 | Branch: [25-cache](/../../tree/25-cache) 288 | 289 | We've done a lot of heavy lifting ourself in our `App\Kernel`. Let `symfony/http-kernel` 290 | be responsible for that from now on. Our `App\Kernel` should extend `Symfony\Component\HttpKernel\Kernel` 291 | but we still need to define where our configuration is located. 292 | 293 | The Symfony kernel uses a `HttpKernel` to handle the request. This is done automatically 294 | if you register a `http_kernel` service: 295 | 296 | ```yaml 297 | http_kernel: 298 | class: Symfony\Component\HttpKernel\HttpKernel 299 | public: true 300 | arguments: 301 | - '@Symfony\Component\EventDispatcher\EventDispatcherInterface' 302 | - '@Symfony\Component\HttpKernel\Controller\ControllerResolver' 303 | 304 | Symfony\Component\HttpKernel\Controller\ControllerResolver: ~ 305 | ``` 306 | 307 | ### Exercise 7: Router 308 | 309 | Branch: [26-http-kernel](/../../tree/26-http-kernel) 310 | 311 | Symfony 4.1 has the quickest router implemented in PHP. Lets start using it. 312 | We want to remove our `Router` middleware and define our routes in `./config/routes.yaml` 313 | instead. We are not using the FrameworkBundle just yet so we need to look at 314 | the documentation for the [routing **component**](http://symfony.com/doc/current/components/routing.html#the-all-in-one-router). 315 | 316 | **Note:** Make sure to register `Symfony\Component\HttpKernel\EventListener\RouterListener` 317 | in the service container. 318 | 319 | ### Exercise 8: Http Kernel (important) 320 | 321 | Branch: [27-router](/../../tree/27-router) 322 | 323 | The `Symfony\Component\HttpKernel\EventListener\RouterListener` listens to the `kernel.request` 324 | event. Debug the [`HttpKernel::handleRaw`](https://github.com/symfony/symfony/blob/v4.1.3/src/Symfony/Component/HttpKernel/HttpKernel.php#L119-L169) 325 | function to see what is happening there. Prepare short answers to the following 326 | questions: 327 | 328 | - What did `RouterListener` do to the `$request` after the `kernel.request` event has 329 | been dispatched? (line 125) 330 | - What is the purpose of `$this->resolver->getController($request)`? (line 132) 331 | - What is the purpose of `$this->argumentResolver->getArguments($request, $controller)`? (line 141) 332 | - What does this line do `$response = \call_user_func_array($controller, $arguments)`? (line 149) 333 | 334 | ### Exercise 9: Framework Bundle 335 | 336 | Branch: [27-router](/../../tree/27-router) 337 | 338 | We are almost there. Let's start using the FrameworkBundle. This bundle helps you register 339 | a lot of services. Make sure to enable the FrameworkBundle in `App\Kernel`. What can 340 | you remove now? Maybe the router configuration? 341 | 342 | You could also have a look at `Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait`. It 343 | could be a good fit for our `App\Kernel`. 344 | 345 | ### Exercise 10: Symfony Flex 346 | 347 | Branch: [28-framework-bundle](/../../tree/28-framework-bundle) 348 | 349 | This is the end of the workshop. You could create a new Symfony Flex probject with 350 | `composer create-project symfony/skeleton my_projecet`. Compare the differencis between a fresh 351 | install of symfony with the framework you built. 352 | 353 | --------------------------------------------------------------------------------