├── .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 |
    6 | {% for submission in submissions %} 7 |
  1. 8 | {{ submission.title }}
    9 | submitted by {{ submission.author }} 10 |
  2. 11 | {% endfor %} 12 |
13 | {% endblock %} -------------------------------------------------------------------------------- /templates/Layout.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Social News 6 | 7 | 8 |

Social News

9 | {% block content %}{% endblock %} 10 | 11 | -------------------------------------------------------------------------------- /templates/Login.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'Layout.html.twig' %} 2 | 3 | {% block content %} 4 | {% include 'FlashMessages.html.twig' %} 5 |
6 | 7 |
8 | 9 |
10 |
12 | 13 |
14 | {% endblock %} -------------------------------------------------------------------------------- /templates/Registration.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'Layout.html.twig' %} 2 | {% block content %} 3 | {% include 'FlashMessages.html.twig' %} 4 |
5 | 6 |
7 | 8 |
9 | 11 | 12 |
13 | {% endblock %} -------------------------------------------------------------------------------- /templates/Submission.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'Layout.html.twig' %} 2 | 3 | {% block content %} 4 | {% include 'FlashMessages.html.twig' %} 5 | 6 |
7 | 8 |
9 | 10 |
11 |
12 | 13 |
14 | {% endblock %} --------------------------------------------------------------------------------