├── .gitignore
├── README.md
├── bin
└── Migrate.php
├── composer.json
├── composer.lock
├── migrations
├── Migration201704151205.php
└── Migration201706040802.php
├── public
└── index.php
├── src
├── Bootstrap.php
├── Dependencies.php
├── Framework
│ ├── Csrf
│ │ ├── StoredTokenReader.php
│ │ ├── StoredTokenValidator.php
│ │ ├── SymfonySessionTokenStorage.php
│ │ ├── Token.php
│ │ └── TokenStorage.php
│ ├── Dbal
│ │ ├── ConnectionFactory.php
│ │ └── DatabaseUrl.php
│ ├── Rbac
│ │ ├── AuthenticatedUser.php
│ │ ├── CurrentUserFactory.php
│ │ ├── Guest.php
│ │ ├── Permission.php
│ │ ├── Permission
│ │ │ └── SubmitLink.php
│ │ ├── Role.php
│ │ ├── Role
│ │ │ └── Author.php
│ │ ├── SymfonySessionCurrentUserFactory.php
│ │ └── User.php
│ └── Rendering
│ │ ├── TemplateDirectory.php
│ │ ├── TemplateRenderer.php
│ │ ├── TwigTemplateRenderer.php
│ │ └── TwigTemplateRendererFactory.php
├── FrontPage
│ ├── Application
│ │ ├── Submission.php
│ │ └── SubmissionsQuery.php
│ ├── Infrastructure
│ │ └── DbalSubmissionsQuery.php
│ └── Presentation
│ │ └── FrontPageController.php
├── Routes.php
├── Submission
│ ├── Application
│ │ ├── SubmitLink.php
│ │ └── SubmitLinkHandler.php
│ ├── Domain
│ │ ├── AuthorId.php
│ │ ├── Submission.php
│ │ └── SubmissionRepository.php
│ ├── Infrastructure
│ │ └── DbalSubmissionRepository.php
│ └── Presentation
│ │ ├── SubmissionController.php
│ │ ├── SubmissionForm.php
│ │ └── SubmissionFormFactory.php
└── User
│ ├── Application
│ ├── LogIn.php
│ ├── LogInHandler.php
│ ├── NicknameTakenQuery.php
│ ├── RegisterUser.php
│ └── RegisterUserHandler.php
│ ├── Domain
│ ├── User.php
│ ├── UserRepository.php
│ └── UserWasLoggedIn.php
│ ├── Infrastructure
│ ├── DbalNicknameTakenQuery.php
│ └── DbalUserRepository.php
│ └── Presentation
│ ├── LoginController.php
│ ├── RegisterUserForm.php
│ ├── RegisterUserFormFactory.php
│ └── RegistrationController.php
├── storage
└── .gitignore
└── templates
├── FlashMessages.html.twig
├── FrontPage.html.twig
├── Layout.html.twig
├── Login.html.twig
├── Registration.html.twig
└── Submission.html.twig
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | vendor
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is the sample code from the book [Professional PHP:
2 | Learn how to build maintainable and secure applications
3 | ](http://patricklouys.com/professional-php/).
4 |
5 | ### Disclaimer
6 |
7 | This code is not ready for production. This repository is just intended as a reference for the readers of the book.
--------------------------------------------------------------------------------
/bin/Migrate.php:
--------------------------------------------------------------------------------
1 | make('Doctrine\DBAL\Connection');
9 |
10 | function getAvailableMigrations(): array
11 | {
12 | $migrations = [];
13 | foreach (new FilesystemIterator(ROOT_DIR . '/migrations') as $file) {
14 | $migrations[] = $file->getBasename('.php');
15 | }
16 | return array_reverse($migrations);
17 | }
18 |
19 | function selectMigration(array $migrations): int
20 | {
21 | echo "[0] All" . PHP_EOL;
22 | foreach ($migrations as $key => $name) {
23 | $index = $key + 1;
24 | echo "[$index] $name" . PHP_EOL;
25 | }
26 | $selected = readline('Select the migration that you want to run: ');
27 | $selectedKey = $selected - 1;
28 | if ($selected !== '0' && !array_key_exists($selectedKey, $migrations)) {
29 | exit('Invalid selection' . PHP_EOL);
30 | }
31 | return (int)$selected;
32 | }
33 |
34 | $migrations = getAvailableMigrations();
35 | $selected = selectMigration($migrations);
36 |
37 | foreach ($migrations as $key => $migration) {
38 | if ($selected !== 0 && $selected !== $key + 1) {
39 | continue;
40 | }
41 | $class = "Migrations\\$migration";
42 | (new $class($connection))->migrate();
43 | echo "Running $migration..." . PHP_EOL;
44 | }
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "require": {
3 | "php": "~7.2.0",
4 | "symfony/http-foundation": "^4.0",
5 | "tracy/tracy": "^2.4",
6 | "nikic/fast-route": "^1.2",
7 | "twig/twig": "^2.4",
8 | "rdlowrey/auryn": "^1.4",
9 | "doctrine/dbal": "^2.6",
10 | "ramsey/uuid": "^3.7"
11 | },
12 | "autoload": {
13 | "psr-4": {
14 | "SocialNews\\": "src/",
15 | "Migrations\\": "migrations/"
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/composer.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_readme": [
3 | "This file locks the dependencies of your project to a known state",
4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
5 | "This file is @generated automatically"
6 | ],
7 | "content-hash": "ea6eb1c8fe4823e5a6774519957528da",
8 | "packages": [
9 | {
10 | "name": "doctrine/annotations",
11 | "version": "v1.5.0",
12 | "source": {
13 | "type": "git",
14 | "url": "https://github.com/doctrine/annotations.git",
15 | "reference": "5beebb01b025c94e93686b7a0ed3edae81fe3e7f"
16 | },
17 | "dist": {
18 | "type": "zip",
19 | "url": "https://api.github.com/repos/doctrine/annotations/zipball/5beebb01b025c94e93686b7a0ed3edae81fe3e7f",
20 | "reference": "5beebb01b025c94e93686b7a0ed3edae81fe3e7f",
21 | "shasum": ""
22 | },
23 | "require": {
24 | "doctrine/lexer": "1.*",
25 | "php": "^7.1"
26 | },
27 | "require-dev": {
28 | "doctrine/cache": "1.*",
29 | "phpunit/phpunit": "^5.7"
30 | },
31 | "type": "library",
32 | "extra": {
33 | "branch-alias": {
34 | "dev-master": "1.5.x-dev"
35 | }
36 | },
37 | "autoload": {
38 | "psr-4": {
39 | "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations"
40 | }
41 | },
42 | "notification-url": "https://packagist.org/downloads/",
43 | "license": [
44 | "MIT"
45 | ],
46 | "authors": [
47 | {
48 | "name": "Roman Borschel",
49 | "email": "roman@code-factory.org"
50 | },
51 | {
52 | "name": "Benjamin Eberlei",
53 | "email": "kontakt@beberlei.de"
54 | },
55 | {
56 | "name": "Guilherme Blanco",
57 | "email": "guilhermeblanco@gmail.com"
58 | },
59 | {
60 | "name": "Jonathan Wage",
61 | "email": "jonwage@gmail.com"
62 | },
63 | {
64 | "name": "Johannes Schmitt",
65 | "email": "schmittjoh@gmail.com"
66 | }
67 | ],
68 | "description": "Docblock Annotations Parser",
69 | "homepage": "http://www.doctrine-project.org",
70 | "keywords": [
71 | "annotations",
72 | "docblock",
73 | "parser"
74 | ],
75 | "time": "2017-07-22T10:58:02+00:00"
76 | },
77 | {
78 | "name": "doctrine/cache",
79 | "version": "v1.7.1",
80 | "source": {
81 | "type": "git",
82 | "url": "https://github.com/doctrine/cache.git",
83 | "reference": "b3217d58609e9c8e661cd41357a54d926c4a2a1a"
84 | },
85 | "dist": {
86 | "type": "zip",
87 | "url": "https://api.github.com/repos/doctrine/cache/zipball/b3217d58609e9c8e661cd41357a54d926c4a2a1a",
88 | "reference": "b3217d58609e9c8e661cd41357a54d926c4a2a1a",
89 | "shasum": ""
90 | },
91 | "require": {
92 | "php": "~7.1"
93 | },
94 | "conflict": {
95 | "doctrine/common": ">2.2,<2.4"
96 | },
97 | "require-dev": {
98 | "alcaeus/mongo-php-adapter": "^1.1",
99 | "mongodb/mongodb": "^1.1",
100 | "phpunit/phpunit": "^5.7",
101 | "predis/predis": "~1.0"
102 | },
103 | "suggest": {
104 | "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver"
105 | },
106 | "type": "library",
107 | "extra": {
108 | "branch-alias": {
109 | "dev-master": "1.7.x-dev"
110 | }
111 | },
112 | "autoload": {
113 | "psr-4": {
114 | "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache"
115 | }
116 | },
117 | "notification-url": "https://packagist.org/downloads/",
118 | "license": [
119 | "MIT"
120 | ],
121 | "authors": [
122 | {
123 | "name": "Roman Borschel",
124 | "email": "roman@code-factory.org"
125 | },
126 | {
127 | "name": "Benjamin Eberlei",
128 | "email": "kontakt@beberlei.de"
129 | },
130 | {
131 | "name": "Guilherme Blanco",
132 | "email": "guilhermeblanco@gmail.com"
133 | },
134 | {
135 | "name": "Jonathan Wage",
136 | "email": "jonwage@gmail.com"
137 | },
138 | {
139 | "name": "Johannes Schmitt",
140 | "email": "schmittjoh@gmail.com"
141 | }
142 | ],
143 | "description": "Caching library offering an object-oriented API for many cache backends",
144 | "homepage": "http://www.doctrine-project.org",
145 | "keywords": [
146 | "cache",
147 | "caching"
148 | ],
149 | "time": "2017-08-25T07:02:50+00:00"
150 | },
151 | {
152 | "name": "doctrine/collections",
153 | "version": "v1.5.0",
154 | "source": {
155 | "type": "git",
156 | "url": "https://github.com/doctrine/collections.git",
157 | "reference": "a01ee38fcd999f34d9bfbcee59dbda5105449cbf"
158 | },
159 | "dist": {
160 | "type": "zip",
161 | "url": "https://api.github.com/repos/doctrine/collections/zipball/a01ee38fcd999f34d9bfbcee59dbda5105449cbf",
162 | "reference": "a01ee38fcd999f34d9bfbcee59dbda5105449cbf",
163 | "shasum": ""
164 | },
165 | "require": {
166 | "php": "^7.1"
167 | },
168 | "require-dev": {
169 | "doctrine/coding-standard": "~0.1@dev",
170 | "phpunit/phpunit": "^5.7"
171 | },
172 | "type": "library",
173 | "extra": {
174 | "branch-alias": {
175 | "dev-master": "1.3.x-dev"
176 | }
177 | },
178 | "autoload": {
179 | "psr-0": {
180 | "Doctrine\\Common\\Collections\\": "lib/"
181 | }
182 | },
183 | "notification-url": "https://packagist.org/downloads/",
184 | "license": [
185 | "MIT"
186 | ],
187 | "authors": [
188 | {
189 | "name": "Roman Borschel",
190 | "email": "roman@code-factory.org"
191 | },
192 | {
193 | "name": "Benjamin Eberlei",
194 | "email": "kontakt@beberlei.de"
195 | },
196 | {
197 | "name": "Guilherme Blanco",
198 | "email": "guilhermeblanco@gmail.com"
199 | },
200 | {
201 | "name": "Jonathan Wage",
202 | "email": "jonwage@gmail.com"
203 | },
204 | {
205 | "name": "Johannes Schmitt",
206 | "email": "schmittjoh@gmail.com"
207 | }
208 | ],
209 | "description": "Collections Abstraction library",
210 | "homepage": "http://www.doctrine-project.org",
211 | "keywords": [
212 | "array",
213 | "collections",
214 | "iterator"
215 | ],
216 | "time": "2017-07-22T10:37:32+00:00"
217 | },
218 | {
219 | "name": "doctrine/common",
220 | "version": "v2.8.1",
221 | "source": {
222 | "type": "git",
223 | "url": "https://github.com/doctrine/common.git",
224 | "reference": "f68c297ce6455e8fd794aa8ffaf9fa458f6ade66"
225 | },
226 | "dist": {
227 | "type": "zip",
228 | "url": "https://api.github.com/repos/doctrine/common/zipball/f68c297ce6455e8fd794aa8ffaf9fa458f6ade66",
229 | "reference": "f68c297ce6455e8fd794aa8ffaf9fa458f6ade66",
230 | "shasum": ""
231 | },
232 | "require": {
233 | "doctrine/annotations": "1.*",
234 | "doctrine/cache": "1.*",
235 | "doctrine/collections": "1.*",
236 | "doctrine/inflector": "1.*",
237 | "doctrine/lexer": "1.*",
238 | "php": "~7.1"
239 | },
240 | "require-dev": {
241 | "phpunit/phpunit": "^5.7"
242 | },
243 | "type": "library",
244 | "extra": {
245 | "branch-alias": {
246 | "dev-master": "2.8.x-dev"
247 | }
248 | },
249 | "autoload": {
250 | "psr-4": {
251 | "Doctrine\\Common\\": "lib/Doctrine/Common"
252 | }
253 | },
254 | "notification-url": "https://packagist.org/downloads/",
255 | "license": [
256 | "MIT"
257 | ],
258 | "authors": [
259 | {
260 | "name": "Roman Borschel",
261 | "email": "roman@code-factory.org"
262 | },
263 | {
264 | "name": "Benjamin Eberlei",
265 | "email": "kontakt@beberlei.de"
266 | },
267 | {
268 | "name": "Guilherme Blanco",
269 | "email": "guilhermeblanco@gmail.com"
270 | },
271 | {
272 | "name": "Jonathan Wage",
273 | "email": "jonwage@gmail.com"
274 | },
275 | {
276 | "name": "Johannes Schmitt",
277 | "email": "schmittjoh@gmail.com"
278 | }
279 | ],
280 | "description": "Common Library for Doctrine projects",
281 | "homepage": "http://www.doctrine-project.org",
282 | "keywords": [
283 | "annotations",
284 | "collections",
285 | "eventmanager",
286 | "persistence",
287 | "spl"
288 | ],
289 | "time": "2017-08-31T08:43:38+00:00"
290 | },
291 | {
292 | "name": "doctrine/dbal",
293 | "version": "v2.6.3",
294 | "source": {
295 | "type": "git",
296 | "url": "https://github.com/doctrine/dbal.git",
297 | "reference": "e3eed9b1facbb0ced3a0995244843a189e7d1b13"
298 | },
299 | "dist": {
300 | "type": "zip",
301 | "url": "https://api.github.com/repos/doctrine/dbal/zipball/e3eed9b1facbb0ced3a0995244843a189e7d1b13",
302 | "reference": "e3eed9b1facbb0ced3a0995244843a189e7d1b13",
303 | "shasum": ""
304 | },
305 | "require": {
306 | "doctrine/common": "^2.7.1",
307 | "ext-pdo": "*",
308 | "php": "^7.1"
309 | },
310 | "require-dev": {
311 | "phpunit/phpunit": "^5.4.6",
312 | "phpunit/phpunit-mock-objects": "!=3.2.4,!=3.2.5",
313 | "symfony/console": "2.*||^3.0"
314 | },
315 | "suggest": {
316 | "symfony/console": "For helpful console commands such as SQL execution and import of files."
317 | },
318 | "bin": [
319 | "bin/doctrine-dbal"
320 | ],
321 | "type": "library",
322 | "extra": {
323 | "branch-alias": {
324 | "dev-master": "2.6.x-dev"
325 | }
326 | },
327 | "autoload": {
328 | "psr-0": {
329 | "Doctrine\\DBAL\\": "lib/"
330 | }
331 | },
332 | "notification-url": "https://packagist.org/downloads/",
333 | "license": [
334 | "MIT"
335 | ],
336 | "authors": [
337 | {
338 | "name": "Roman Borschel",
339 | "email": "roman@code-factory.org"
340 | },
341 | {
342 | "name": "Benjamin Eberlei",
343 | "email": "kontakt@beberlei.de"
344 | },
345 | {
346 | "name": "Guilherme Blanco",
347 | "email": "guilhermeblanco@gmail.com"
348 | },
349 | {
350 | "name": "Jonathan Wage",
351 | "email": "jonwage@gmail.com"
352 | }
353 | ],
354 | "description": "Database Abstraction Layer",
355 | "homepage": "http://www.doctrine-project.org",
356 | "keywords": [
357 | "database",
358 | "dbal",
359 | "persistence",
360 | "queryobject"
361 | ],
362 | "time": "2017-11-19T13:38:54+00:00"
363 | },
364 | {
365 | "name": "doctrine/inflector",
366 | "version": "v1.2.0",
367 | "source": {
368 | "type": "git",
369 | "url": "https://github.com/doctrine/inflector.git",
370 | "reference": "e11d84c6e018beedd929cff5220969a3c6d1d462"
371 | },
372 | "dist": {
373 | "type": "zip",
374 | "url": "https://api.github.com/repos/doctrine/inflector/zipball/e11d84c6e018beedd929cff5220969a3c6d1d462",
375 | "reference": "e11d84c6e018beedd929cff5220969a3c6d1d462",
376 | "shasum": ""
377 | },
378 | "require": {
379 | "php": "^7.0"
380 | },
381 | "require-dev": {
382 | "phpunit/phpunit": "^6.2"
383 | },
384 | "type": "library",
385 | "extra": {
386 | "branch-alias": {
387 | "dev-master": "1.2.x-dev"
388 | }
389 | },
390 | "autoload": {
391 | "psr-4": {
392 | "Doctrine\\Common\\Inflector\\": "lib/Doctrine/Common/Inflector"
393 | }
394 | },
395 | "notification-url": "https://packagist.org/downloads/",
396 | "license": [
397 | "MIT"
398 | ],
399 | "authors": [
400 | {
401 | "name": "Roman Borschel",
402 | "email": "roman@code-factory.org"
403 | },
404 | {
405 | "name": "Benjamin Eberlei",
406 | "email": "kontakt@beberlei.de"
407 | },
408 | {
409 | "name": "Guilherme Blanco",
410 | "email": "guilhermeblanco@gmail.com"
411 | },
412 | {
413 | "name": "Jonathan Wage",
414 | "email": "jonwage@gmail.com"
415 | },
416 | {
417 | "name": "Johannes Schmitt",
418 | "email": "schmittjoh@gmail.com"
419 | }
420 | ],
421 | "description": "Common String Manipulations with regard to casing and singular/plural rules.",
422 | "homepage": "http://www.doctrine-project.org",
423 | "keywords": [
424 | "inflection",
425 | "pluralize",
426 | "singularize",
427 | "string"
428 | ],
429 | "time": "2017-07-22T12:18:28+00:00"
430 | },
431 | {
432 | "name": "doctrine/lexer",
433 | "version": "v1.0.1",
434 | "source": {
435 | "type": "git",
436 | "url": "https://github.com/doctrine/lexer.git",
437 | "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c"
438 | },
439 | "dist": {
440 | "type": "zip",
441 | "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c",
442 | "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c",
443 | "shasum": ""
444 | },
445 | "require": {
446 | "php": ">=5.3.2"
447 | },
448 | "type": "library",
449 | "extra": {
450 | "branch-alias": {
451 | "dev-master": "1.0.x-dev"
452 | }
453 | },
454 | "autoload": {
455 | "psr-0": {
456 | "Doctrine\\Common\\Lexer\\": "lib/"
457 | }
458 | },
459 | "notification-url": "https://packagist.org/downloads/",
460 | "license": [
461 | "MIT"
462 | ],
463 | "authors": [
464 | {
465 | "name": "Roman Borschel",
466 | "email": "roman@code-factory.org"
467 | },
468 | {
469 | "name": "Guilherme Blanco",
470 | "email": "guilhermeblanco@gmail.com"
471 | },
472 | {
473 | "name": "Johannes Schmitt",
474 | "email": "schmittjoh@gmail.com"
475 | }
476 | ],
477 | "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.",
478 | "homepage": "http://www.doctrine-project.org",
479 | "keywords": [
480 | "lexer",
481 | "parser"
482 | ],
483 | "time": "2014-09-09T13:34:57+00:00"
484 | },
485 | {
486 | "name": "nikic/fast-route",
487 | "version": "v1.2.0",
488 | "source": {
489 | "type": "git",
490 | "url": "https://github.com/nikic/FastRoute.git",
491 | "reference": "b5f95749071c82a8e0f58586987627054400cdf6"
492 | },
493 | "dist": {
494 | "type": "zip",
495 | "url": "https://api.github.com/repos/nikic/FastRoute/zipball/b5f95749071c82a8e0f58586987627054400cdf6",
496 | "reference": "b5f95749071c82a8e0f58586987627054400cdf6",
497 | "shasum": ""
498 | },
499 | "require": {
500 | "php": ">=5.4.0"
501 | },
502 | "type": "library",
503 | "autoload": {
504 | "psr-4": {
505 | "FastRoute\\": "src/"
506 | },
507 | "files": [
508 | "src/functions.php"
509 | ]
510 | },
511 | "notification-url": "https://packagist.org/downloads/",
512 | "license": [
513 | "BSD-3-Clause"
514 | ],
515 | "authors": [
516 | {
517 | "name": "Nikita Popov",
518 | "email": "nikic@php.net"
519 | }
520 | ],
521 | "description": "Fast request router for PHP",
522 | "keywords": [
523 | "router",
524 | "routing"
525 | ],
526 | "time": "2017-01-19T11:35:12+00:00"
527 | },
528 | {
529 | "name": "paragonie/random_compat",
530 | "version": "v2.0.11",
531 | "source": {
532 | "type": "git",
533 | "url": "https://github.com/paragonie/random_compat.git",
534 | "reference": "5da4d3c796c275c55f057af5a643ae297d96b4d8"
535 | },
536 | "dist": {
537 | "type": "zip",
538 | "url": "https://api.github.com/repos/paragonie/random_compat/zipball/5da4d3c796c275c55f057af5a643ae297d96b4d8",
539 | "reference": "5da4d3c796c275c55f057af5a643ae297d96b4d8",
540 | "shasum": ""
541 | },
542 | "require": {
543 | "php": ">=5.2.0"
544 | },
545 | "require-dev": {
546 | "phpunit/phpunit": "4.*|5.*"
547 | },
548 | "suggest": {
549 | "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
550 | },
551 | "type": "library",
552 | "autoload": {
553 | "files": [
554 | "lib/random.php"
555 | ]
556 | },
557 | "notification-url": "https://packagist.org/downloads/",
558 | "license": [
559 | "MIT"
560 | ],
561 | "authors": [
562 | {
563 | "name": "Paragon Initiative Enterprises",
564 | "email": "security@paragonie.com",
565 | "homepage": "https://paragonie.com"
566 | }
567 | ],
568 | "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
569 | "keywords": [
570 | "csprng",
571 | "pseudorandom",
572 | "random"
573 | ],
574 | "time": "2017-09-27T21:40:39+00:00"
575 | },
576 | {
577 | "name": "ramsey/uuid",
578 | "version": "3.7.1",
579 | "source": {
580 | "type": "git",
581 | "url": "https://github.com/ramsey/uuid.git",
582 | "reference": "45cffe822057a09e05f7bd09ec5fb88eeecd2334"
583 | },
584 | "dist": {
585 | "type": "zip",
586 | "url": "https://api.github.com/repos/ramsey/uuid/zipball/45cffe822057a09e05f7bd09ec5fb88eeecd2334",
587 | "reference": "45cffe822057a09e05f7bd09ec5fb88eeecd2334",
588 | "shasum": ""
589 | },
590 | "require": {
591 | "paragonie/random_compat": "^1.0|^2.0",
592 | "php": "^5.4 || ^7.0"
593 | },
594 | "replace": {
595 | "rhumsaa/uuid": "self.version"
596 | },
597 | "require-dev": {
598 | "apigen/apigen": "^4.1",
599 | "codeception/aspect-mock": "^1.0 | ^2.0",
600 | "doctrine/annotations": "~1.2.0",
601 | "goaop/framework": "1.0.0-alpha.2 | ^1.0 | ^2.1",
602 | "ircmaxell/random-lib": "^1.1",
603 | "jakub-onderka/php-parallel-lint": "^0.9.0",
604 | "mockery/mockery": "^0.9.4",
605 | "moontoast/math": "^1.1",
606 | "php-mock/php-mock-phpunit": "^0.3|^1.1",
607 | "phpunit/phpunit": "^4.7|>=5.0 <5.4",
608 | "satooshi/php-coveralls": "^0.6.1",
609 | "squizlabs/php_codesniffer": "^2.3"
610 | },
611 | "suggest": {
612 | "ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator",
613 | "ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator",
614 | "ircmaxell/random-lib": "Provides RandomLib for use with the RandomLibAdapter",
615 | "moontoast/math": "Provides support for converting UUID to 128-bit integer (in string form).",
616 | "ramsey/uuid-console": "A console application for generating UUIDs with ramsey/uuid",
617 | "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type."
618 | },
619 | "type": "library",
620 | "extra": {
621 | "branch-alias": {
622 | "dev-master": "3.x-dev"
623 | }
624 | },
625 | "autoload": {
626 | "psr-4": {
627 | "Ramsey\\Uuid\\": "src/"
628 | }
629 | },
630 | "notification-url": "https://packagist.org/downloads/",
631 | "license": [
632 | "MIT"
633 | ],
634 | "authors": [
635 | {
636 | "name": "Marijn Huizendveld",
637 | "email": "marijn.huizendveld@gmail.com"
638 | },
639 | {
640 | "name": "Thibaud Fabre",
641 | "email": "thibaud@aztech.io"
642 | },
643 | {
644 | "name": "Ben Ramsey",
645 | "email": "ben@benramsey.com",
646 | "homepage": "https://benramsey.com"
647 | }
648 | ],
649 | "description": "Formerly rhumsaa/uuid. A PHP 5.4+ library for generating RFC 4122 version 1, 3, 4, and 5 universally unique identifiers (UUID).",
650 | "homepage": "https://github.com/ramsey/uuid",
651 | "keywords": [
652 | "guid",
653 | "identifier",
654 | "uuid"
655 | ],
656 | "time": "2017-09-22T20:46:04+00:00"
657 | },
658 | {
659 | "name": "rdlowrey/auryn",
660 | "version": "v1.4.2",
661 | "source": {
662 | "type": "git",
663 | "url": "https://github.com/rdlowrey/auryn.git",
664 | "reference": "8c4dc07943599ba84f4f89eab8cf43efeef80395"
665 | },
666 | "dist": {
667 | "type": "zip",
668 | "url": "https://api.github.com/repos/rdlowrey/auryn/zipball/8c4dc07943599ba84f4f89eab8cf43efeef80395",
669 | "reference": "8c4dc07943599ba84f4f89eab8cf43efeef80395",
670 | "shasum": ""
671 | },
672 | "require": {
673 | "php": ">=5.3.0"
674 | },
675 | "require-dev": {
676 | "athletic/athletic": "~0.1",
677 | "fabpot/php-cs-fixer": "~1.9",
678 | "phpunit/phpunit": "^4.7"
679 | },
680 | "type": "library",
681 | "autoload": {
682 | "psr-4": {
683 | "Auryn\\": "lib/"
684 | }
685 | },
686 | "notification-url": "https://packagist.org/downloads/",
687 | "license": [
688 | "MIT"
689 | ],
690 | "authors": [
691 | {
692 | "name": "Dan Ackroyd",
693 | "email": "Danack@basereality.com",
694 | "homepage": "http://www.basereality.com",
695 | "role": "Developer"
696 | },
697 | {
698 | "name": "Levi Morrison",
699 | "email": "levim@php.net",
700 | "homepage": "http://morrisonlevi.github.com/",
701 | "role": "Developer"
702 | },
703 | {
704 | "name": "Daniel Lowrey",
705 | "email": "rdlowrey@gmail.com",
706 | "homepage": "https://github.com/rdlowrey",
707 | "role": "Creator / Main Developer"
708 | }
709 | ],
710 | "description": "Auryn is a dependency injector for bootstrapping object-oriented PHP applications.",
711 | "homepage": "https://github.com/rdlowrey/auryn",
712 | "keywords": [
713 | "dependency injection",
714 | "dic",
715 | "ioc"
716 | ],
717 | "time": "2017-05-15T06:26:46+00:00"
718 | },
719 | {
720 | "name": "symfony/http-foundation",
721 | "version": "v4.0.0",
722 | "source": {
723 | "type": "git",
724 | "url": "https://github.com/symfony/http-foundation.git",
725 | "reference": "40a9400633675adafbc94302004f9ec04589210f"
726 | },
727 | "dist": {
728 | "type": "zip",
729 | "url": "https://api.github.com/repos/symfony/http-foundation/zipball/40a9400633675adafbc94302004f9ec04589210f",
730 | "reference": "40a9400633675adafbc94302004f9ec04589210f",
731 | "shasum": ""
732 | },
733 | "require": {
734 | "php": "^7.1.3",
735 | "symfony/polyfill-mbstring": "~1.1"
736 | },
737 | "require-dev": {
738 | "symfony/expression-language": "~3.4|~4.0"
739 | },
740 | "type": "library",
741 | "extra": {
742 | "branch-alias": {
743 | "dev-master": "4.0-dev"
744 | }
745 | },
746 | "autoload": {
747 | "psr-4": {
748 | "Symfony\\Component\\HttpFoundation\\": ""
749 | },
750 | "exclude-from-classmap": [
751 | "/Tests/"
752 | ]
753 | },
754 | "notification-url": "https://packagist.org/downloads/",
755 | "license": [
756 | "MIT"
757 | ],
758 | "authors": [
759 | {
760 | "name": "Fabien Potencier",
761 | "email": "fabien@symfony.com"
762 | },
763 | {
764 | "name": "Symfony Community",
765 | "homepage": "https://symfony.com/contributors"
766 | }
767 | ],
768 | "description": "Symfony HttpFoundation Component",
769 | "homepage": "https://symfony.com",
770 | "time": "2017-11-30T15:11:43+00:00"
771 | },
772 | {
773 | "name": "symfony/polyfill-mbstring",
774 | "version": "v1.6.0",
775 | "source": {
776 | "type": "git",
777 | "url": "https://github.com/symfony/polyfill-mbstring.git",
778 | "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296"
779 | },
780 | "dist": {
781 | "type": "zip",
782 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
783 | "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
784 | "shasum": ""
785 | },
786 | "require": {
787 | "php": ">=5.3.3"
788 | },
789 | "suggest": {
790 | "ext-mbstring": "For best performance"
791 | },
792 | "type": "library",
793 | "extra": {
794 | "branch-alias": {
795 | "dev-master": "1.6-dev"
796 | }
797 | },
798 | "autoload": {
799 | "psr-4": {
800 | "Symfony\\Polyfill\\Mbstring\\": ""
801 | },
802 | "files": [
803 | "bootstrap.php"
804 | ]
805 | },
806 | "notification-url": "https://packagist.org/downloads/",
807 | "license": [
808 | "MIT"
809 | ],
810 | "authors": [
811 | {
812 | "name": "Nicolas Grekas",
813 | "email": "p@tchwork.com"
814 | },
815 | {
816 | "name": "Symfony Community",
817 | "homepage": "https://symfony.com/contributors"
818 | }
819 | ],
820 | "description": "Symfony polyfill for the Mbstring extension",
821 | "homepage": "https://symfony.com",
822 | "keywords": [
823 | "compatibility",
824 | "mbstring",
825 | "polyfill",
826 | "portable",
827 | "shim"
828 | ],
829 | "time": "2017-10-11T12:05:26+00:00"
830 | },
831 | {
832 | "name": "tracy/tracy",
833 | "version": "v2.4.10",
834 | "source": {
835 | "type": "git",
836 | "url": "https://github.com/nette/tracy.git",
837 | "reference": "5b302790edd71924dfe4ec44f499ef61df3f53a2"
838 | },
839 | "dist": {
840 | "type": "zip",
841 | "url": "https://api.github.com/repos/nette/tracy/zipball/5b302790edd71924dfe4ec44f499ef61df3f53a2",
842 | "reference": "5b302790edd71924dfe4ec44f499ef61df3f53a2",
843 | "shasum": ""
844 | },
845 | "require": {
846 | "ext-json": "*",
847 | "ext-session": "*",
848 | "php": ">=5.4.4"
849 | },
850 | "require-dev": {
851 | "nette/di": "~2.3",
852 | "nette/tester": "~1.7"
853 | },
854 | "suggest": {
855 | "https://nette.org/donate": "Please support Tracy via a donation"
856 | },
857 | "type": "library",
858 | "extra": {
859 | "branch-alias": {
860 | "dev-master": "2.4-dev"
861 | }
862 | },
863 | "autoload": {
864 | "classmap": [
865 | "src"
866 | ],
867 | "files": [
868 | "src/shortcuts.php"
869 | ]
870 | },
871 | "notification-url": "https://packagist.org/downloads/",
872 | "license": [
873 | "BSD-3-Clause",
874 | "GPL-2.0",
875 | "GPL-3.0"
876 | ],
877 | "authors": [
878 | {
879 | "name": "David Grudl",
880 | "homepage": "https://davidgrudl.com"
881 | },
882 | {
883 | "name": "Nette Community",
884 | "homepage": "https://nette.org/contributors"
885 | }
886 | ],
887 | "description": "😎 Tracy: the addictive tool to ease debugging PHP code for cool developers. Friendly design, logging, profiler, advanced features like debugging AJAX calls or CLI support. You will love it.",
888 | "homepage": "https://tracy.nette.org",
889 | "keywords": [
890 | "Xdebug",
891 | "debug",
892 | "debugger",
893 | "nette",
894 | "profiler"
895 | ],
896 | "time": "2017-10-04T18:43:42+00:00"
897 | },
898 | {
899 | "name": "twig/twig",
900 | "version": "v2.4.4",
901 | "source": {
902 | "type": "git",
903 | "url": "https://github.com/twigphp/Twig.git",
904 | "reference": "eddb97148ad779f27e670e1e3f19fb323aedafeb"
905 | },
906 | "dist": {
907 | "type": "zip",
908 | "url": "https://api.github.com/repos/twigphp/Twig/zipball/eddb97148ad779f27e670e1e3f19fb323aedafeb",
909 | "reference": "eddb97148ad779f27e670e1e3f19fb323aedafeb",
910 | "shasum": ""
911 | },
912 | "require": {
913 | "php": "^7.0",
914 | "symfony/polyfill-mbstring": "~1.0"
915 | },
916 | "require-dev": {
917 | "psr/container": "^1.0",
918 | "symfony/debug": "~2.7",
919 | "symfony/phpunit-bridge": "~3.3@dev"
920 | },
921 | "type": "library",
922 | "extra": {
923 | "branch-alias": {
924 | "dev-master": "2.4-dev"
925 | }
926 | },
927 | "autoload": {
928 | "psr-0": {
929 | "Twig_": "lib/"
930 | },
931 | "psr-4": {
932 | "Twig\\": "src/"
933 | }
934 | },
935 | "notification-url": "https://packagist.org/downloads/",
936 | "license": [
937 | "BSD-3-Clause"
938 | ],
939 | "authors": [
940 | {
941 | "name": "Fabien Potencier",
942 | "email": "fabien@symfony.com",
943 | "homepage": "http://fabien.potencier.org",
944 | "role": "Lead Developer"
945 | },
946 | {
947 | "name": "Armin Ronacher",
948 | "email": "armin.ronacher@active-4.com",
949 | "role": "Project Founder"
950 | },
951 | {
952 | "name": "Twig Team",
953 | "homepage": "http://twig.sensiolabs.org/contributors",
954 | "role": "Contributors"
955 | }
956 | ],
957 | "description": "Twig, the flexible, fast, and secure template language for PHP",
958 | "homepage": "http://twig.sensiolabs.org",
959 | "keywords": [
960 | "templating"
961 | ],
962 | "time": "2017-09-27T18:10:31+00:00"
963 | }
964 | ],
965 | "packages-dev": [],
966 | "aliases": [],
967 | "minimum-stability": "stable",
968 | "stability-flags": [],
969 | "prefer-stable": false,
970 | "prefer-lowest": false,
971 | "platform": {
972 | "php": "~7.2.0"
973 | },
974 | "platform-dev": []
975 | }
976 |
--------------------------------------------------------------------------------
/migrations/Migration201704151205.php:
--------------------------------------------------------------------------------
1 | connection = $connection;
16 | }
17 |
18 | public function migrate(): void
19 | {
20 | $schema = new Schema();
21 | $this->createSubmissionsTable($schema);
22 | $queries = $schema->toSql($this->connection->getDatabasePlatform());
23 | foreach ($queries as $query) {
24 | $this->connection->executeQuery($query);
25 | }
26 | }
27 |
28 | private function createSubmissionsTable(Schema $schema): void
29 | {
30 | $table = $schema->createTable('submissions');
31 | $table->addColumn('id', Type::GUID);
32 | $table->addColumn('title', Type::STRING);
33 | $table->addColumn('url', Type::STRING);
34 | $table->addColumn('creation_date', Type::DATETIME);
35 | $table->addColumn('author_user_id', Type::GUID);
36 | }
37 | }
--------------------------------------------------------------------------------
/migrations/Migration201706040802.php:
--------------------------------------------------------------------------------
1 | connection = $connection;
16 | }
17 |
18 | public function migrate(): void
19 | {
20 | $schema = new Schema();
21 | $this->createUsersTable($schema);
22 |
23 | $queries = $schema->toSql($this->connection->getDatabasePlatform());
24 | foreach ($queries as $query) {
25 | $this->connection->executeQuery($query);
26 | }
27 | }
28 |
29 | private function createUsersTable(Schema $schema): void
30 | {
31 | $table = $schema->createTable('users');
32 | $table->addColumn('id', Type::GUID);
33 | $table->addColumn('nickname', Type::STRING);
34 | $table->addColumn('password_hash', TYPE::STRING);
35 | $table->addColumn('creation_date', TYPE::DATETIME);
36 | $table->addColumn('failed_login_attempts', TYPE::INTEGER, [
37 | 'default' => 0,
38 | ]);
39 | $table->addColumn('last_failed_login_attempt', TYPE::DATETIME, [
40 | 'notnull' => false,
41 | ]);
42 | }
43 | }
--------------------------------------------------------------------------------
/public/index.php:
--------------------------------------------------------------------------------
1 | addRoute(...$route);
16 | }
17 | }
18 | );
19 |
20 | $routeInfo = $dispatcher->dispatch(
21 | $request->getMethod(),
22 | $request->getPathInfo()
23 | );
24 |
25 | switch ($routeInfo[0]) {
26 | case \FastRoute\Dispatcher::NOT_FOUND:
27 | $response = new \Symfony\Component\HttpFoundation\Response(
28 | 'Not found',
29 | 404
30 | );
31 | break;
32 | case \FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
33 | $response = new \Symfony\Component\HttpFoundation\Response(
34 | 'Method not allowed',
35 | 405
36 | );
37 | break;
38 | case \FastRoute\Dispatcher::FOUND:
39 | [$controllerName, $method] = explode('#', $routeInfo[1]);
40 | $vars = $routeInfo[2];
41 | $injector = include('Dependencies.php');
42 | $controller = $injector->make($controllerName);
43 | $response = $controller->$method($request, $vars);
44 | break;
45 | }
46 |
47 | if (!$response instanceof \Symfony\Component\HttpFoundation\Response) {
48 | throw new \Exception('Controller methods must return a Response object');
49 | }
50 |
51 | $response->prepare($request);
52 | $response->send();
--------------------------------------------------------------------------------
/src/Dependencies.php:
--------------------------------------------------------------------------------
1 | delegate(
28 | TemplateRenderer::class,
29 | function () use ($injector): TemplateRenderer {
30 | $factory = $injector->make(TwigTemplateRendererFactory::class);
31 | return $factory->create();
32 | }
33 | );
34 |
35 | $injector->define(TemplateDirectory::class, [':rootDirectory' => ROOT_DIR]);
36 |
37 | $injector->define(
38 | DatabaseUrl::class,
39 | [':url' => 'sqlite:///' . ROOT_DIR . '/storage/db.sqlite3']
40 | );
41 |
42 | $injector->delegate(Connection::class, function () use ($injector): Connection {
43 | $factory = $injector->make(ConnectionFactory::class);
44 | return $factory->create();
45 | });
46 | $injector->share(Connection::class);
47 |
48 | $injector->alias(SubmissionsQuery::class, DbalSubmissionsQuery::class);
49 | $injector->share(SubmissionsQuery::class);
50 |
51 | $injector->alias(TokenStorage::class, SymfonySessionTokenStorage::class);
52 |
53 | $injector->alias(SessionInterface::class, Session::class);
54 |
55 | $injector->alias(SubmissionRepository::class, DbalSubmissionRepository::class);
56 |
57 | $injector->alias(UserRepository::class, DbalUserRepository::class);
58 |
59 | $injector->alias(NicknameTakenQuery::class, DbalNicknameTakenQuery::class);
60 |
61 | $injector->delegate(User::class, function () use ($injector): User {
62 | $factory = $injector->make(SymfonySessionCurrentUserFactory::class);
63 | return $factory->create();
64 | });
65 |
66 | return $injector;
--------------------------------------------------------------------------------
/src/Framework/Csrf/StoredTokenReader.php:
--------------------------------------------------------------------------------
1 | tokenStorage = $tokenStorage;
12 | }
13 |
14 | public function read(string $key): Token
15 | {
16 | $token = $this->tokenStorage->retrieve($key);
17 |
18 | if ($token !== null) {
19 | return $token;
20 | }
21 |
22 | $token = Token::generate();
23 | $this->tokenStorage->store($key, $token);
24 |
25 | return $token;
26 | }
27 | }
--------------------------------------------------------------------------------
/src/Framework/Csrf/StoredTokenValidator.php:
--------------------------------------------------------------------------------
1 | tokenStorage = $tokenStorage;
12 | }
13 |
14 | public function validate(string $key, Token $token): bool
15 | {
16 | $storedToken = $this->tokenStorage->retrieve($key);
17 | if ($storedToken === null) {
18 | return false;
19 | }
20 | return $token->equals($storedToken);
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Framework/Csrf/SymfonySessionTokenStorage.php:
--------------------------------------------------------------------------------
1 | session = $session;
14 | }
15 |
16 | public function store(string $key, Token $token): void
17 | {
18 | $this->session->set($key, $token->toString());
19 | }
20 |
21 | public function retrieve(string $key): ?Token
22 | {
23 | $tokenValue = $this->session->get($key);
24 |
25 | if ($tokenValue === null) {
26 | return null;
27 | }
28 |
29 | return new Token($tokenValue);
30 | }
31 | }
--------------------------------------------------------------------------------
/src/Framework/Csrf/Token.php:
--------------------------------------------------------------------------------
1 | token = $token;
12 | }
13 |
14 | public function toString(): string
15 | {
16 | return $this->token;
17 | }
18 |
19 | public static function generate(): Token
20 | {
21 | $token = bin2hex(random_bytes(256));
22 | return new Token($token);
23 | }
24 |
25 | public function equals(Token $token): bool
26 | {
27 | return ($this->token === $token->toString());
28 | }
29 | }
--------------------------------------------------------------------------------
/src/Framework/Csrf/TokenStorage.php:
--------------------------------------------------------------------------------
1 | databaseUrl = $databaseUrl;
16 | }
17 |
18 | public function create(): Connection
19 | {
20 | return DriverManager::getConnection(
21 | ['url' => $this->databaseUrl->toString()],
22 | new Configuration()
23 | );
24 | }
25 | }
--------------------------------------------------------------------------------
/src/Framework/Dbal/DatabaseUrl.php:
--------------------------------------------------------------------------------
1 | url = $url;
12 | }
13 |
14 | public function toString(): string
15 | {
16 | return $this->url;
17 | }
18 | }
--------------------------------------------------------------------------------
/src/Framework/Rbac/AuthenticatedUser.php:
--------------------------------------------------------------------------------
1 | id = $id;
18 | $this->roles = $roles;
19 | }
20 |
21 | public function getId(): UuidInterface
22 | {
23 | return $this->id;
24 | }
25 |
26 | public function hasPermission(Permission $permission): bool
27 | {
28 | foreach ($this->roles as $role) {
29 | if ($role->hasPermission($permission)) {
30 | return true;
31 | }
32 | }
33 | return false;
34 | }
35 | }
--------------------------------------------------------------------------------
/src/Framework/Rbac/CurrentUserFactory.php:
--------------------------------------------------------------------------------
1 | getPermissions());
10 | }
11 |
12 | /**
13 | * @return Permission[]
14 | */
15 | abstract protected function getPermissions(): array;
16 | }
--------------------------------------------------------------------------------
/src/Framework/Rbac/Role/Author.php:
--------------------------------------------------------------------------------
1 | session = $session;
16 | }
17 |
18 | public function create(): User
19 | {
20 | if (!$this->session->has('userId')) {
21 | return new Guest();
22 | }
23 |
24 | return new AuthenticatedUser(
25 | Uuid::fromString($this->session->get('userId')),
26 | [new Author()]
27 | );
28 | }
29 | }
--------------------------------------------------------------------------------
/src/Framework/Rbac/User.php:
--------------------------------------------------------------------------------
1 | templateDirectory = $rootDirectory . '/templates';
12 | }
13 |
14 | public function toString(): string
15 | {
16 | return $this->templateDirectory;
17 | }
18 | }
--------------------------------------------------------------------------------
/src/Framework/Rendering/TemplateRenderer.php:
--------------------------------------------------------------------------------
1 | twigEnvironment = $twigEnvironment;
14 | }
15 |
16 | public function render(string $template, array $data = []): string
17 | {
18 | return $this->twigEnvironment->render($template, $data);
19 | }
20 | }
--------------------------------------------------------------------------------
/src/Framework/Rendering/TwigTemplateRendererFactory.php:
--------------------------------------------------------------------------------
1 | templateDirectory = $templateDirectory;
24 | $this->storedTokenReader = $storedTokenReader;
25 | $this->session = $session;
26 | }
27 |
28 | public function create(): TwigTemplateRenderer
29 | {
30 | $loader = new Twig_Loader_Filesystem([
31 | $this->templateDirectory->toString(),
32 | ]);
33 | $twigEnvironment = new Twig_Environment($loader);
34 |
35 | $twigEnvironment->addFunction(
36 | new Twig_Function('get_token', function (string $key): string {
37 | $token = $this->storedTokenReader->read($key);
38 | return $token->toString();
39 | })
40 | );
41 |
42 | $twigEnvironment->addFunction(
43 | new Twig_Function('get_flash_bag', function (): FlashBagInterface {
44 | return $this->session->getFlashBag();
45 | })
46 | );
47 |
48 | return new TwigTemplateRenderer($twigEnvironment);
49 | }
50 | }
--------------------------------------------------------------------------------
/src/FrontPage/Application/Submission.php:
--------------------------------------------------------------------------------
1 | url = $url;
14 | $this->title = $title;
15 | $this->author = $author;
16 | }
17 |
18 | public function getUrl(): string
19 | {
20 | return $this->url;
21 | }
22 |
23 | public function getTitle(): string
24 | {
25 | return $this->title;
26 | }
27 |
28 | public function getAuthor(): string
29 | {
30 | return $this->author;
31 | }
32 | }
--------------------------------------------------------------------------------
/src/FrontPage/Application/SubmissionsQuery.php:
--------------------------------------------------------------------------------
1 | connection = $connection;
17 | }
18 |
19 | public function execute(): array
20 | {
21 | $qb = $this->connection->createQueryBuilder();
22 |
23 | $qb->addSelect('submissions.title');
24 | $qb->addSelect('submissions.url');
25 | $qb->addSelect('authors.nickname');
26 | $qb->from('submissions');
27 | $qb->join(
28 | 'submissions',
29 | 'users',
30 | 'authors',
31 | 'submissions.author_user_id = authors.id'
32 | );
33 | $qb->orderBy('submissions.creation_date', 'DESC');
34 |
35 | $stmt = $qb->execute();
36 | $rows = $stmt->fetchAll();
37 |
38 | $submissions = [];
39 | foreach ($rows as $row) {
40 | $submissions[] = new Submission($row['url'], $row['title'], $row['nickname']);
41 | }
42 | return $submissions;
43 | }
44 | }
--------------------------------------------------------------------------------
/src/FrontPage/Presentation/FrontPageController.php:
--------------------------------------------------------------------------------
1 | templateRenderer = $templateRenderer;
19 | $this->submissionsQuery = $submissionsQuery;
20 | }
21 |
22 | public function show(): Response
23 | {
24 | $content = $this->templateRenderer->render('FrontPage.html.twig', [
25 | 'submissions' => $this->submissionsQuery->execute(),
26 | ]);
27 | return new Response($content);
28 | }
29 | }
--------------------------------------------------------------------------------
/src/Routes.php:
--------------------------------------------------------------------------------
1 | authorId = $authorId;
16 | $this->url = $url;
17 | $this->title = $title;
18 | }
19 |
20 | public function getAuthorId(): UuidInterface
21 | {
22 | return $this->authorId;
23 | }
24 |
25 | public function getUrl(): string
26 | {
27 | return $this->url;
28 | }
29 |
30 | public function getTitle(): string
31 | {
32 | return $this->title;
33 | }
34 | }
--------------------------------------------------------------------------------
/src/Submission/Application/SubmitLinkHandler.php:
--------------------------------------------------------------------------------
1 | submissionRepository = $submissionRepository;
15 | }
16 |
17 | public function handle(SubmitLink $command): void
18 | {
19 | $submission = Submission::submit(
20 | $command->getAuthorId(),
21 | $command->getUrl(),
22 | $command->getTitle()
23 | );
24 | $this->submissionRepository->add($submission);
25 | }
26 | }
--------------------------------------------------------------------------------
/src/Submission/Domain/AuthorId.php:
--------------------------------------------------------------------------------
1 | id = $id;
14 | }
15 |
16 | public static function fromUuid(UuidInterface $uuid): AuthorId
17 | {
18 | return new AuthorId($uuid->toString());
19 | }
20 |
21 | public function toString(): string
22 | {
23 | return $this->id;
24 | }
25 | }
--------------------------------------------------------------------------------
/src/Submission/Domain/Submission.php:
--------------------------------------------------------------------------------
1 | id = $id;
25 | $this->authorId = $authorId;
26 | $this->url = $url;
27 | $this->title = $title;
28 | $this->creationDate = $creationDate;
29 | }
30 |
31 | public static function submit(
32 | UuidInterface $authorId,
33 | string $url,
34 | string $title
35 | ): Submission {
36 | return new Submission(
37 | Uuid::uuid4(),
38 | AuthorId::fromUuid($authorId),
39 | $url,
40 | $title,
41 | new DateTimeImmutable()
42 | );
43 | }
44 |
45 | public function getId(): UuidInterface
46 | {
47 | return $this->id;
48 | }
49 |
50 | public function getUrl(): string
51 | {
52 | return $this->url;
53 | }
54 |
55 | public function getTitle(): string
56 | {
57 | return $this->title;
58 | }
59 |
60 | public function getCreationDate(): DateTimeImmutable
61 | {
62 | return $this->creationDate;
63 | }
64 |
65 | public function getAuthorId(): AuthorId
66 | {
67 | return $this->authorId;
68 | }
69 | }
--------------------------------------------------------------------------------
/src/Submission/Domain/SubmissionRepository.php:
--------------------------------------------------------------------------------
1 | connection = $connection;
16 | }
17 |
18 | public function add(Submission $submission): void
19 | {
20 | $qb = $this->connection->createQueryBuilder();
21 |
22 | $qb->insert('submissions');
23 | $qb->values([
24 | 'id' => $qb->createNamedParameter($submission->getId()->toString()),
25 | 'title' => $qb->createNamedParameter($submission->getTitle()),
26 | 'url' => $qb->createNamedParameter($submission->getUrl()),
27 | 'creation_date' => $qb->createNamedParameter(
28 | $submission->getCreationDate(),
29 | 'datetime'
30 | ),
31 | 'author_user_id' => $qb->createNamedParameter(
32 | $submission->getAuthorId()->toString()
33 | ),
34 | ]);
35 |
36 | $qb->execute();
37 | }
38 | }
--------------------------------------------------------------------------------
/src/Submission/Presentation/SubmissionController.php:
--------------------------------------------------------------------------------
1 | templateRenderer = $templateRenderer;
31 | $this->submissionFormFactory = $submissionFormFactory;
32 | $this->session = $session;
33 | $this->submitLinkHandler = $submitLinkHandler;
34 | $this->user = $user;
35 | }
36 |
37 | public function show(): Response
38 | {
39 | if (!$this->user->hasPermission(new Permission\SubmitLink())) {
40 | $this->session->getFlashBag()->add(
41 | 'errors',
42 | 'You have to log in before you can submit a link.'
43 | );
44 | return new RedirectResponse('/login');
45 | }
46 |
47 | $content = $this->templateRenderer->render('Submission.html.twig');
48 | return new Response($content);
49 | }
50 |
51 | public function submit(Request $request): Response
52 | {
53 | if (!$this->user->hasPermission(new Permission\SubmitLink())) {
54 | $this->session->getFlashBag()->add(
55 | 'errors',
56 | 'You have to log in before you can submit a link.'
57 | );
58 | return new RedirectResponse('/login');
59 | }
60 |
61 | $response = new RedirectResponse('/submit');
62 | $form = $this->submissionFormFactory->createFromRequest($request);
63 | if ($form->hasValidationErrors()) {
64 | foreach ($form->getValidationErrors() as $errorMessage) {
65 | $this->session->getFlashBag()->add('errors', $errorMessage);
66 | }
67 | return $response;
68 | }
69 |
70 | if (!$this->user instanceof AuthenticatedUser) {
71 | throw new \LogicException('Only authenticated users can submit links');
72 | }
73 | $this->submitLinkHandler->handle($form->toCommand($this->user));
74 |
75 | $this->session->getFlashBag()->add(
76 | 'success',
77 | 'Your URL was submitted successfully'
78 | );
79 | return $response;
80 | }
81 | }
--------------------------------------------------------------------------------
/src/Submission/Presentation/SubmissionForm.php:
--------------------------------------------------------------------------------
1 | storedTokenValidator = $storedTokenValidator;
24 | $this->token = $token;
25 | $this->title = $title;
26 | $this->url = $url;
27 | }
28 |
29 | /**
30 | * @return string[]
31 | */
32 | public function getValidationErrors(): array
33 | {
34 | $errors = [];
35 | if (!$this->storedTokenValidator->validate(
36 | 'submission',
37 | new Token($this->token)
38 | )) {
39 | $errors[] = 'Invalid token';
40 | }
41 | if (strlen($this->title) < 1 || strlen($this->title) > 200) {
42 | $errors[] = 'Title must be between 1 and 200 characters';
43 | }
44 | if (strlen($this->url) < 1 || strlen($this->url) > 200) {
45 | $errors[] = 'URL must be between 1 and 200 characters';
46 | }
47 | return $errors;
48 | }
49 |
50 | public function hasValidationErrors(): bool
51 | {
52 | return (count($this->getValidationErrors()) > 0);
53 | }
54 |
55 | public function toCommand(AuthenticatedUser $author): SubmitLink
56 | {
57 | return new SubmitLink($author->getId(), $this->url, $this->title);
58 | }
59 | }
--------------------------------------------------------------------------------
/src/Submission/Presentation/SubmissionFormFactory.php:
--------------------------------------------------------------------------------
1 | storedTokenValidator = $storedTokenValidator;
15 | }
16 |
17 | public function createFromRequest(Request $request): SubmissionForm
18 | {
19 | return new SubmissionForm(
20 | $this->storedTokenValidator,
21 | (string)$request->get('token'),
22 | (string)$request->get('title'),
23 | (string)$request->get('url')
24 | );
25 | }
26 | }
--------------------------------------------------------------------------------
/src/User/Application/LogIn.php:
--------------------------------------------------------------------------------
1 | nickname = $nickname;
13 | $this->password = $password;
14 | }
15 |
16 | public function getNickname(): string
17 | {
18 | return $this->nickname;
19 | }
20 |
21 | public function getPassword(): string
22 | {
23 | return $this->password;
24 | }
25 | }
--------------------------------------------------------------------------------
/src/User/Application/LogInHandler.php:
--------------------------------------------------------------------------------
1 | userRepository = $userRepository;
14 | }
15 |
16 | public function handle(LogIn $command): void
17 | {
18 | $user = $this->userRepository->findByNickname($command->getNickname());
19 | if ($user === null) {
20 | return;
21 | }
22 | $user->logIn($command->getPassword());
23 | $this->userRepository->save($user);
24 | }
25 | }
--------------------------------------------------------------------------------
/src/User/Application/NicknameTakenQuery.php:
--------------------------------------------------------------------------------
1 | nickname = $nickname;
13 | $this->password = $password;
14 | }
15 |
16 | public function getNickname(): string
17 | {
18 | return $this->nickname;
19 | }
20 |
21 | public function getPassword(): string
22 | {
23 | return $this->password;
24 | }
25 | }
--------------------------------------------------------------------------------
/src/User/Application/RegisterUserHandler.php:
--------------------------------------------------------------------------------
1 | userRepository = $userRepository;
15 | }
16 |
17 | public function handle(RegisterUser $command): void
18 | {
19 | $user = User::register(
20 | $command->getNickname(),
21 | $command->getPassword()
22 | );
23 | $this->userRepository->add($user);
24 | }
25 | }
--------------------------------------------------------------------------------
/src/User/Domain/User.php:
--------------------------------------------------------------------------------
1 | id = $id;
28 | $this->nickname = $nickname;
29 | $this->passwordHash = $passwordHash;
30 | $this->creationDate = $creationDate;
31 | $this->failedLoginAttempts = $failedLoginAttempts;
32 | $this->lastFailedLoginAttempt = $lastFailedLoginAttempt;
33 | }
34 |
35 | public static function register(string $nickname, string $password): User
36 | {
37 | return new User(
38 | Uuid::uuid4(),
39 | $nickname,
40 | password_hash($password, PASSWORD_DEFAULT),
41 | new DateTimeImmutable(),
42 | 0,
43 | null
44 | );
45 | }
46 |
47 | public function logIn(string $password): void
48 | {
49 | if (!password_verify($password, $this->passwordHash)) {
50 | $this->lastFailedLoginAttempt = new DateTimeImmutable();
51 | $this->failedLoginAttempts++;
52 | return;
53 | }
54 | $this->failedLoginAttempts = 0;
55 | $this->lastFailedLoginAttempt = null;
56 | $this->recordedEvents[] = new UserWasLoggedIn();
57 | }
58 |
59 | public function getId(): UuidInterface
60 | {
61 | return $this->id;
62 | }
63 |
64 | public function getNickname(): string
65 | {
66 | return $this->nickname;
67 | }
68 |
69 | public function getPasswordHash(): string
70 | {
71 | return $this->passwordHash;
72 | }
73 |
74 | public function getCreationDate(): DateTimeImmutable
75 | {
76 | return $this->creationDate;
77 | }
78 |
79 | public function getFailedLoginAttempts(): int
80 | {
81 | return $this->failedLoginAttempts;
82 | }
83 |
84 | public function getLastFailedLoginAttempt(): ?DateTimeImmutable
85 | {
86 | return $this->lastFailedLoginAttempt;
87 | }
88 |
89 | public function getRecordedEvents(): array
90 | {
91 | return $this->recordedEvents;
92 | }
93 |
94 | public function clearRecordedEvents(): void
95 | {
96 | $this->recordedEvents = [];
97 | }
98 | }
--------------------------------------------------------------------------------
/src/User/Domain/UserRepository.php:
--------------------------------------------------------------------------------
1 | connection = $connection;
15 | }
16 |
17 | public function execute(string $nickname): bool
18 | {
19 | $qb = $this->connection->createQueryBuilder();
20 |
21 | $qb->select('count(*)');
22 | $qb->from('users');
23 | $qb->where("nickname = {$qb->createNamedParameter($nickname)}");
24 | $qb->execute();
25 |
26 | $stmt = $qb->execute();
27 | return (bool)$stmt->fetchColumn();
28 | }
29 | }
--------------------------------------------------------------------------------
/src/User/Infrastructure/DbalUserRepository.php:
--------------------------------------------------------------------------------
1 | connection = $connection;
23 | $this->session = $session;
24 | }
25 |
26 | public function add(User $user): void
27 | {
28 | $qb = $this->connection->createQueryBuilder();
29 |
30 | $qb->insert('users');
31 | $qb->values([
32 | 'id' => $qb->createNamedParameter($user->getId()->toString()),
33 | 'nickname' => $qb->createNamedParameter($user->getNickname()),
34 | 'password_hash' => $qb->createNamedParameter(
35 | $user->getPasswordHash()
36 | ),
37 | 'creation_date' => $qb->createNamedParameter(
38 | $user->getCreationDate(),
39 | Type::DATETIME
40 | ),
41 | ]);
42 |
43 | $qb->execute();
44 | }
45 |
46 | public function save(User $user): void
47 | {
48 | foreach ($user->getRecordedEvents() as $event) {
49 | if ($event instanceof UserWasLoggedIn) {
50 | $this->session->set('userId', $user->getId()->toString());
51 | continue;
52 | }
53 | throw new LogicException(get_class($event) . ' was not handled');
54 | }
55 | $user->clearRecordedEvents();
56 |
57 | $qb = $this->connection->createQueryBuilder();
58 |
59 | $qb->update('users');
60 | $qb->set('nickname', $qb->createNamedParameter($user->getNickname()));
61 | $qb->set('password_hash', $qb->createNamedParameter(
62 | $user->getPasswordHash()
63 | ));
64 | $qb->set('failed_login_attempts', $qb->createNamedParameter(
65 | $user->getFailedLoginAttempts()
66 | ));
67 | $qb->set('last_failed_login_attempt', $qb->createNamedParameter(
68 | $user->getLastFailedLoginAttempt(),
69 | Type::DATETIME
70 | ));
71 |
72 | $qb->execute();
73 | }
74 |
75 | public function findByNickname(string $nickname): ?User
76 | {
77 | $qb = $this->connection->createQueryBuilder();
78 |
79 | $qb->addSelect('id');
80 | $qb->addSelect('nickname');
81 | $qb->addSelect('password_hash');
82 | $qb->addSelect('creation_date');
83 | $qb->addSelect('failed_login_attempts');
84 | $qb->addSelect('last_failed_login_attempt');
85 | $qb->from('users');
86 | $qb->where("nickname = {$qb->createNamedParameter($nickname)}");
87 |
88 | $stmt = $qb->execute();
89 | $row = $stmt->fetch();
90 |
91 | if (!$row) {
92 | return null;
93 | }
94 |
95 | return $this->createUserFromRow($row);
96 | }
97 |
98 | private function createUserFromRow(array $row): ?User
99 | {
100 | if (!$row) {
101 | return null;
102 | }
103 | $lastFailedLoginAttempt = null;
104 | if ($row['last_failed_login_attempt']) {
105 | $lastFailedLoginAttempt = new DateTimeImmutable(
106 | $row['last_failed_login_attempt']
107 | );
108 | }
109 | return new User(
110 | Uuid::fromString($row['id']),
111 | $row['nickname'],
112 | $row['password_hash'],
113 | new DateTimeImmutable($row['creation_date']),
114 | (int)$row['failed_login_attempts'],
115 | $lastFailedLoginAttempt
116 | );
117 | }
118 | }
--------------------------------------------------------------------------------
/src/User/Presentation/LoginController.php:
--------------------------------------------------------------------------------
1 | templateRenderer = $templateRenderer;
29 | $this->storedTokenValidator = $storedTokenValidator;
30 | $this->session = $session;
31 | $this->logInHandler = $logInHandler;
32 | }
33 |
34 | public function show(): Response
35 | {
36 | $content = $this->templateRenderer->render('Login.html.twig');
37 | return new Response($content);
38 | }
39 |
40 | public function logIn(Request $request): Response
41 | {
42 | $this->session->remove('userId');
43 |
44 | if (!$this->storedTokenValidator->validate(
45 | 'login',
46 | new Token((string)$request->get('token'))
47 | )) {
48 | $this->session->getFlashBag()->add('errors', 'Invalid token');
49 | return new RedirectResponse('/login');
50 | }
51 |
52 | $this->logInHandler->handle(new LogIn(
53 | (string)$request->get('nickname'),
54 | (string)$request->get('password')
55 | ));
56 |
57 | if ($this->session->get('userId') === null) {
58 | $this->session->getFlashBag()->add('errors', 'Invalid username or password');
59 | return new RedirectResponse('/login');
60 | }
61 |
62 | $this->session->getFlashBag()->add('success', 'You were logged in.');
63 | return new RedirectResponse('/');
64 | }
65 | }
--------------------------------------------------------------------------------
/src/User/Presentation/RegisterUserForm.php:
--------------------------------------------------------------------------------
1 | storedTokenValidator = $storedTokenValidator;
26 | $this->token = $token;
27 | $this->nickname = $nickname;
28 | $this->password = $password;
29 | $this->nicknameTakenQuery = $nicknameTakenQuery;
30 | }
31 |
32 | public function hasValidationErrors(): bool
33 | {
34 | return (count($this->getValidationErrors()) > 0);
35 | }
36 |
37 | /**
38 | * @return string[]
39 | */
40 | public function getValidationErrors(): array
41 | {
42 | $errors = [];
43 |
44 | if (!$this->storedTokenValidator->validate(
45 | 'registration',
46 | new Token($this->token)
47 | )) {
48 | $errors[] = 'Invalid token';
49 | }
50 |
51 | if (strlen($this->nickname) < 3 || strlen($this->nickname) > 20) {
52 | $errors[] = 'Nickname must be between 3 and 20 characters';
53 | }
54 |
55 | if (!ctype_alnum($this->nickname)) {
56 | $errors[] = 'Nickname can only consist of letters and numbers';
57 | }
58 |
59 | if ($this->nicknameTakenQuery->execute($this->nickname)) {
60 | $errors[] = 'This nickname is already being used';
61 | }
62 |
63 | if (strlen($this->password) < 8) {
64 | $errors[] = 'Password must be at least 8 characters';
65 | }
66 |
67 | return $errors;
68 | }
69 |
70 | public function toCommand(): RegisterUser
71 | {
72 | return new RegisterUser($this->nickname, $this->password);
73 | }
74 | }
--------------------------------------------------------------------------------
/src/User/Presentation/RegisterUserFormFactory.php:
--------------------------------------------------------------------------------
1 | storedTokenValidator = $storedTokenValidator;
19 | $this->nicknameTakenQuery = $nicknameTakenQuery;
20 | }
21 |
22 | public function createFromRequest(Request $request): RegisterUserForm
23 | {
24 | return new RegisterUserForm(
25 | $this->storedTokenValidator,
26 | $this->nicknameTakenQuery,
27 | (string)$request->get('token'),
28 | (string)$request->get('nickname'),
29 | (string)$request->get('password')
30 | );
31 | }
32 | }
--------------------------------------------------------------------------------
/src/User/Presentation/RegistrationController.php:
--------------------------------------------------------------------------------
1 | templateRenderer = $templateRenderer;
26 | $this->registerUserFormFactory = $registerUserFormFactory;
27 | $this->session = $session;
28 | $this->registerUserHandler = $registerUserHandler;
29 | }
30 |
31 | public function show(): Response
32 | {
33 | $content = $this->templateRenderer->render('Registration.html.twig');
34 | return new Response($content);
35 | }
36 |
37 | public function register(Request $request): Response
38 | {
39 | $response = new RedirectResponse('/register');
40 | $form = $this->registerUserFormFactory->createFromRequest($request);
41 |
42 | if ($form->hasValidationErrors()) {
43 | foreach ($form->getValidationErrors() as $errorMessage) {
44 | $this->session->getFlashBag()->add('errors', $errorMessage);
45 | }
46 | return $response;
47 | }
48 |
49 | $this->registerUserHandler->handle($form->toCommand());
50 |
51 | $this->session->getFlashBag()->add(
52 | 'success',
53 | 'Your account was created. You can now log in.'
54 | );
55 | return $response;
56 | }
57 | }
--------------------------------------------------------------------------------
/storage/.gitignore:
--------------------------------------------------------------------------------
1 | db.sqlite3
--------------------------------------------------------------------------------
/templates/FlashMessages.html.twig:
--------------------------------------------------------------------------------
1 | {% for message in get_flash_bag().get('errors') %}
2 | {{ message }}
3 | {% endfor %}
4 |
5 | {% for message in get_flash_bag().get('success') %}
6 | {{ message }}
7 | {% endfor %}
--------------------------------------------------------------------------------
/templates/FrontPage.html.twig:
--------------------------------------------------------------------------------
1 | {% extends 'Layout.html.twig' %}
2 |
3 | {% block content %}
4 | {% include 'FlashMessages.html.twig' %}
5 |