├── .env.dist ├── .env.test ├── .gitignore ├── .php_cs.dist ├── .travis.yml ├── EXAMPLES.md ├── LICENSE ├── README.md ├── bin ├── console └── phpunit ├── composer.json ├── config ├── bundles.php ├── jwt │ ├── private.pem │ ├── private.pem-back │ └── public.pem ├── packages │ ├── dev │ │ ├── framework.yaml │ │ ├── hautelook_alice.yaml │ │ ├── jms_serializer.yaml │ │ ├── nelmio_alice.yaml │ │ ├── routing.yaml │ │ └── web_profiler.yaml │ ├── doctrine.yaml │ ├── doctrine_migrations.yaml │ ├── framework.yaml │ ├── jms_serializer.yaml │ ├── lexik_jwt_authentication.yaml │ ├── nelmio_api_doc.yaml │ ├── prod │ │ ├── doctrine.yaml │ │ └── jms_serializer.yaml │ ├── routing.yaml │ ├── security.yaml │ ├── security_checker.yaml │ ├── sensio_framework_extra.yaml │ ├── stof_doctrine_extensions.yaml │ ├── test │ │ ├── framework.yaml │ │ ├── hautelook_alice.yaml │ │ ├── nelmio_alice.yaml │ │ ├── routing.yaml │ │ └── web_profiler.yaml │ ├── translation.yaml │ ├── twig.yaml │ └── validator.yaml ├── routes.yaml ├── routes │ ├── annotations.yaml │ ├── dev │ │ ├── twig.yaml │ │ └── web_profiler.yaml │ └── nelmio_api_doc.yaml └── services.yaml ├── css └── style.css ├── fixtures ├── .gitignore ├── data.yml └── data │ ├── books.yml │ ├── movies.yml │ ├── reviews.yml │ └── users.yml ├── phpcs.xml.dist ├── phpmd.dist.xml ├── phpunit.xml.dist ├── public ├── css │ ├── roboto.css │ └── style.css ├── favicon.ico ├── favicons │ ├── android-icon-144x144.png │ ├── android-icon-192x192.png │ ├── android-icon-36x36.png │ ├── android-icon-48x48.png │ ├── android-icon-72x72.png │ ├── android-icon-96x96.png │ ├── apple-icon-114x114.png │ ├── apple-icon-120x120.png │ ├── apple-icon-144x144.png │ ├── apple-icon-152x152.png │ ├── apple-icon-180x180.png │ ├── apple-icon-57x57.png │ ├── apple-icon-60x60.png │ ├── apple-icon-72x72.png │ ├── apple-icon-76x76.png │ ├── apple-icon-precomposed.png │ ├── apple-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── manifest.json │ ├── ms-icon-144x144.png │ ├── ms-icon-150x150.png │ ├── ms-icon-310x310.png │ └── ms-icon-70x70.png ├── images │ └── github.png ├── index.php ├── js │ ├── clusterize.min.js │ └── functions.js └── robots.txt ├── src ├── Controller │ ├── AbstractController.php │ ├── BookController.php │ ├── MovieController.php │ ├── README.md │ ├── ReviewController.php │ └── UserController.php ├── Entity │ ├── AbstractUser.php │ ├── Book.php │ ├── Movie.php │ ├── README.md │ ├── Review.php │ └── User.php ├── EventSubscriber │ ├── README.md │ └── UserSubscriber.php ├── Exception │ ├── ApiException.php │ ├── Enum │ │ └── ApiErrorEnumType.php │ └── FormInvalidException.php ├── Form │ ├── BookType.php │ ├── Filter │ │ ├── BookFilter.php │ │ ├── MovieFilter.php │ │ ├── ReviewFilter.php │ │ └── UserFilter.php │ ├── Handler │ │ ├── AbstractFormHandler.php │ │ └── DefaultFormHandler.php │ ├── MovieType.php │ ├── README.md │ ├── ReviewType.php │ └── UserType.php ├── Interfaces │ ├── ControllerInterface.php │ └── RepositoryInterface.php ├── Kernel.php ├── Migrations │ └── .gitignore ├── Repository │ ├── AbstractRepository.php │ ├── BookRepository.php │ ├── MovieRepository.php │ ├── README.md │ ├── ReviewRepository.php │ └── UserRepository.php ├── Resource │ ├── PaginationResource.php │ └── README.md ├── Security │ ├── README.md │ ├── UserProvider.php │ └── Voter │ │ ├── Book │ │ ├── CreateBookVoter.php │ │ ├── DeleteBookVoter.php │ │ └── UpdateBookVoter.php │ │ ├── Movie │ │ ├── CreateMovieVoter.php │ │ ├── DeleteMovieVoter.php │ │ └── UpdateMovieVoter.php │ │ ├── Review │ │ ├── CreateReviewVoter.php │ │ ├── DeleteReviewVoter.php │ │ └── UpdateReviewVoter.php │ │ └── User │ │ ├── DeleteUserVoter.php │ │ └── UpdateUserVoter.php ├── Service │ ├── Form │ │ └── FormErrorsSerializer.php │ ├── Generic │ │ ├── ResponseCreator.php │ │ └── SerializationService.php │ ├── Manager │ │ └── UserManager.php │ └── README.md └── Traits │ ├── ControllerTrait.php │ ├── IdColumnTrait.php │ ├── README.md │ └── TimeAwareTrait.php ├── templates ├── base.html.twig └── bundles │ ├── NelmioApiDocBundle │ └── SwaggerUi │ │ └── index.html.twig │ └── TwigBundle │ └── Exception │ ├── error404.html.twig │ └── error404.json.twig ├── tests └── Controller │ ├── AbstractWebTestCase.php │ ├── BookControllerTest.php │ ├── MovieControllerTest.php │ ├── ReviewControllerTest.php │ └── UserControllerTest.php ├── translations └── .gitignore └── var └── data.db /.env.dist: -------------------------------------------------------------------------------- 1 | # This file is a "template" of which env vars need to be defined for your application 2 | # Copy this file to .env file for development, create environment variables when deploying to production 3 | # https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration 4 | 5 | ###> symfony/framework-bundle ### 6 | APP_ENV=prod 7 | APP_SECRET=a8fa11779665bac09ad226d42e67c3e9 8 | #TRUSTED_PROXIES=127.0.0.1,127.0.0.2 9 | #TRUSTED_HOSTS=localhost,example.com 10 | ###< symfony/framework-bundle ### 11 | 12 | ###> doctrine/doctrine-bundle ### 13 | # Format described at http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url 14 | # For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db" 15 | # Configure your db driver and server_version in config/packages/doctrine.yaml 16 | #DATABASE_URL=postgres://symfony:symfony@postgres/symfony 17 | DATABASE_URL=sqlite:///%kernel.project_dir%/var/data.db 18 | ###< doctrine/doctrine-bundle ### 19 | 20 | ###> lexik/jwt-authentication-bundle ### 21 | JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem 22 | JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem 23 | JWT_PASSPHRASE=75cc06031b56099860d4213dadb18289 24 | ###< lexik/jwt-authentication-bundle ### 25 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # define your env variables for the test env here 2 | KERNEL_CLASS='App\Kernel' 3 | APP_SECRET='s$cretf0rt3st' 4 | SYMFONY_DEPRECATIONS_HELPER=999999 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ###> symfony/framework-bundle ### 3 | /.env 4 | /.php_cs.cache/ 5 | /public/bundles/ 6 | /var/ 7 | /vendor/ 8 | composer.lock 9 | symfony.lock 10 | ###< symfony/framework-bundle ### 11 | 12 | ###> lexik/jwt-authentication-bundle ### 13 | /config/jwt/*.pem 14 | ###< lexik/jwt-authentication-bundle ### 15 | 16 | ###> friendsofphp/php-cs-fixer ### 17 | /.php_cs 18 | /.php_cs.cache 19 | ###< friendsofphp/php-cs-fixer ### 20 | 21 | ###> symfony/framework-bundle ### 22 | /public/bundles/ 23 | /var/ 24 | /vendor/ 25 | ###< symfony/framework-bundle ### 26 | 27 | ###> symfony/phpunit-bridge ### 28 | .phpunit 29 | /phpunit.xml 30 | ###< symfony/phpunit-bridge ### 31 | 32 | ###> friendsofphp/php-cs-fixer ### 33 | /.php_cs 34 | /.php_cs.cache 35 | ###< friendsofphp/php-cs-fixer ### 36 | 37 | ###> squizlabs/php_codesniffer ### 38 | /.phpcs-cache 39 | /phpcs.xml 40 | ###< squizlabs/php_codesniffer ### 41 | 42 | ###> symfony/web-server-bundle ### 43 | /.web-server-pid 44 | ###< symfony/web-server-bundle ### 45 | -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 11 | ->setRules( 12 | [ 13 | '@Symfony' => true, 14 | '@Symfony:risky' => true, 15 | '@PHP71Migration' => true, 16 | '@PHPUnit48Migration:risky' => true, 17 | 'array_syntax' => ['syntax' => 'short'], 18 | 'dir_constant' => true, 19 | 'heredoc_to_nowdoc' => true, 20 | 'linebreak_after_opening_tag' => true, 21 | 'modernize_types_casting' => true, 22 | 'no_multiline_whitespace_before_semicolons' => true, 23 | 'no_unreachable_default_argument_value' => true, 24 | 'no_useless_else' => true, 25 | 'no_useless_return' => true, 26 | 'ordered_class_elements' => true, 27 | 'ordered_imports' => true, 28 | 'phpdoc_add_missing_param_annotation' => ['only_untyped' => false], 29 | 'phpdoc_order' => true, 30 | 'declare_strict_types' => true, 31 | 'doctrine_annotation_braces' => true, 32 | 'doctrine_annotation_indentation' => true, 33 | 'doctrine_annotation_spaces' => true, 34 | 'psr4' => true, 35 | 'no_php4_constructor' => true, 36 | 'no_short_echo_tag' => true, 37 | 'semicolon_after_instruction' => true, 38 | 'align_multiline_comment' => true, 39 | 'doctrine_annotation_array_assignment' => true, 40 | 'general_phpdoc_annotation_remove' => ['annotations' => ['author', 'package']], 41 | 'list_syntax' => ['syntax' => 'short'], 42 | 'phpdoc_types_order' => ['null_adjustment' => 'always_last'], 43 | 'single_line_comment_style' => true, 44 | 'fully_qualified_strict_types' => true, 45 | 'phpdoc_align' => ['align' => 'left'], 46 | 'no_short_bool_cast' => false, 47 | ] 48 | ) 49 | ->setFinder( 50 | PhpCsFixer\Finder::create() 51 | ->in(__DIR__.'/src') 52 | ); 53 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | sudo: false 3 | 4 | cache: 5 | directories: 6 | - $HOME/.composer/cache/files 7 | - ./bin/.phpunit 8 | 9 | env: 10 | global: 11 | - SYMFONY_PHPUNIT_DIR=./bin/.phpunit 12 | 13 | matrix: 14 | fast_finish: true 15 | include: 16 | - php: 7.2 17 | 18 | before_install: 19 | - '[[ "$TRAVIS_PHP_VERSION" == "7.2" ]] || phpenv config-rm xdebug.ini' 20 | - composer self-update 21 | 22 | install: 23 | - COMPOSER_MEMORY_LIMIT=-1 composer install 24 | - ./bin/phpunit install 25 | 26 | script: 27 | - ./bin/phpunit 28 | # this checks that the source code follows the Symfony Code Syntax rules 29 | - '[[ "$TRAVIS_PHP_VERSION" == "7.2" ]] || ./vendor/bin/php-cs-fixer fix --dry-run -v phpcs.xml.dist' 30 | - ./bin/console lint:yaml config 31 | # this checks that the application doesn't use dependencies with known security vulnerabilities 32 | - ./bin/console security:check 33 | # this checks that the composer.json and composer.lock files are valid 34 | - composer validate 35 | # this checks that Doctrine's mapping configurations are valid 36 | - ./bin/console doctrine:schema:validate --skip-sync -vvv --no-interaction 37 | - '[[ "$TRAVIS_PHP_VERSION" == "7.2" ]] || ./vendor/bin/phpmd src/ text phpmd.dist.xml' 38 | - '[[ "$TRAVIS_PHP_VERSION" == "7.2" ]] || ./vendor/bin/phpstan analyse src' 39 | -------------------------------------------------------------------------------- /EXAMPLES.md: -------------------------------------------------------------------------------- 1 | # Examples of Usage 2 | 3 | **Notice:** Don't forget to add `Content-Type: application/json` to your requests. 4 | 5 | 6 | **Get JWT token:** 7 | 8 | ``` 9 | { 10 | "username": "developer@symfony.local", 11 | "password": "developer" 12 | } 13 | ``` 14 | 15 | **Get list of all books** 16 | 17 | ``` 18 | [GET] http://[host]/books 19 | ``` 20 | 21 | **Get second page the list** 22 | 23 | ``` 24 | [GET] http://[host]/books?page=2 25 | ``` 26 | 27 | By default if Request don't have`limit` parameter Response will return 10 results. 28 | 29 | **Get 20 results per page** 30 | 31 | ``` 32 | [GET] http://[host]/books?limit=20 33 | ``` 34 | 35 | You can combine freely combine all available parameters. 36 | 37 | ``` 38 | [GET] http://[host]/books?limit=20&page=2 39 | ``` 40 | 41 | **Get books with its reviews** 42 | You can also expand book listing of it's reviews. 43 | 44 | ``` 45 | [GET] http://[host]/books?expand=reviews 46 | [GET] http://[host]/books?expand=reviews&limit=20&page=2 47 | ``` 48 | 49 | *Add a new book* 50 | 51 | ``` 52 | [POST] http://[host]/books 53 | 54 | { "isbn": "9799325573620", 55 | "title": "Accusamus nihil repellat vero omnis.", 56 | "description": "Amet et et suscipit qui recusandae totam. Quam ipsam voluptatem cupiditate sed natus debitis voluptas. Laudantium sit repudiandae esse perspiciatis dignissimos error et itaque. Tempora velit porro ut velit soluta explicabo eligendi.", 57 | "author": "Serena Streich", 58 | "publicationDate": "2008-05-10T01:29:03+00:00" 59 | } 60 | 61 | ``` 62 | 63 | ## Filtering 64 | **Get Book of given ISBN** 65 | 66 | ``` 67 | [GET] http://[host]/books?book_filter[isbn]=9799325573620 68 | ``` 69 | 70 | ISBN is Book unique property - if book will not be find it return 404 error. 71 | 72 | **Get books published between certain date:** 73 | 74 | ``` 75 | [GET] http://[host]/books?book_filter[publicationDate][left_datetime]=2017-06-24&[publicationDate][right_datetime]=2018-06-24 76 | ``` 77 | 78 | **Get Users who watched movie of given title** 79 | 80 | ``` 81 | [GET] http://[host]/users?user_filter[movies]=Et aut esse. 82 | [GET] http://[host]/users?expand=movies&user_filter[movies]=Et aut esse. 83 | ``` 84 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Lukasz D. Tulikowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

14 | 15 | # Symfony 4 REST API 16 |

17 | Written WITHOUT FOSUserBundle and FOSRestBundle

18 | 2000 lines under your control. 19 | 20 |
21 |
22 | Requirements: PHP min. version 7.2.0 23 |
24 |
25 | See demo: http://rest-api.tulik.io 26 |

27 | 28 | ## Quick start 29 | 30 | **Clone repository** 31 | 32 | ``` 33 | git clone git@github.com:tulik/symfony-4-rest-api.git 34 | ``` 35 | 36 | **Install dependencies** 37 | 38 | ``` 39 | composer install 40 | ``` 41 | 42 | **Start local server** 43 | 44 | ``` 45 | bin/console server:start 46 | ``` 47 | 48 | ## Listing with filters and pagination 49 | It is possible filtering data using **LexikFormFilterBundle** and to paginate results using **whiteoctober/Pagerfanta** 50 | 51 | ## Flexibility 52 | The whole API including contains **only ~2000 lines of code**, gives you full control possibility to adapt it to an existing project with ease. 53 | 54 | ## Extensibility 55 | Extending its functionality of additional **ElasticSearch**, **Redis** or **RabbitMQ** solution is straightforward. In case you need to change something it's always under your 56 | 57 |

58 | See examples of usage 59 |

60 | 61 | 62 |

63 | 64 | # Documentation 65 | 1. [Controllers](../../tree/master/src/Controller) 66 | 2. [Entities](../../tree/master/src/Entity) 67 | 3. [EventSubscriber](../../tree/master/src/EventSubscriber) 68 | 4. [Form](../../tree/master/src/Form) 69 | 5. [Resource](../../tree/master/src/Resource) 70 | 6. [Security](../../tree/master/src/Security) 71 | 7. [Service](../../tree/master/src/Service) 72 | 8. [Traits](../../tree/master/src/Traits) 73 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | load(__DIR__.'/../.env'); 23 | } 24 | 25 | $input = new ArgvInput(); 26 | $env = $input->getParameterOption(['--env', '-e'], $_SERVER['APP_ENV'] ?? 'dev'); 27 | $debug = ($_SERVER['APP_DEBUG'] ?? ('prod' !== $env)) && !$input->hasParameterOption(['--no-debug', '']); 28 | 29 | if ($debug) { 30 | umask(0000); 31 | 32 | if (class_exists(Debug::class)) { 33 | Debug::enable(); 34 | } 35 | } 36 | 37 | $kernel = new Kernel($env, $debug); 38 | $application = new Application($kernel); 39 | $application->run($input); 40 | -------------------------------------------------------------------------------- /bin/phpunit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | ['all' => true], 5 | Doctrine\Bundle\DoctrineCacheBundle\DoctrineCacheBundle::class => ['all' => true], 6 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 7 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 8 | Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true], 9 | Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true], 10 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], 11 | Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], 12 | JMS\SerializerBundle\JMSSerializerBundle::class => ['all' => true], 13 | Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true], 14 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 15 | Lexik\Bundle\FormFilterBundle\LexikFormFilterBundle::class => ['all' => true], 16 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], 17 | Nelmio\Alice\Bridge\Symfony\NelmioAliceBundle::class => ['dev' => true, 'test' => true], 18 | Fidry\AliceDataFixtures\Bridge\Symfony\FidryAliceDataFixturesBundle::class => ['dev' => true, 'test' => true], 19 | Hautelook\AliceBundle\HautelookAliceBundle::class => ['dev' => true, 'test' => true], 20 | WhiteOctober\PagerfantaBundle\WhiteOctoberPagerfantaBundle::class => ['all' => true], 21 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], 22 | Symfony\Bundle\WebServerBundle\WebServerBundle::class => ['dev' => true], 23 | ]; 24 | -------------------------------------------------------------------------------- /config/jwt/private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKQIBAAKCAgEApwU9utb3UgNg3eygg8rTxNHVWJpYLjghT9saV7IjZV6Mow6p 3 | gZCvmgdC+oqEbouhDymX3g9HLyTvEFhb68bZS27jvtlTtmIHLIDkiBOTZMaNjMcu 4 | RCKD10sG/cShIc13LOdZvyyS8JvAVKmT6swP11mrPlQdwY8kaESXsj/1aMY+yj1r 5 | ENLwRnOI7pMBsWxwlGuMNxu59mFWNzoIdIRNLShu98IQ6rJTNQWhq4hLJfp0ZDhq 6 | p5X4qnie2a9TIMsABfBxQOFjTvBT5nDx+FClI8itQm9bvxYI/D8gR6zUo3DiP3GW 7 | NeSM3TEyqCugPv52p7hrVI+7Pu7QrP1HFZ9+hBgiHiMBrjMcq/UKjqb3gmGMT/I0 8 | bMX3yAk7ErRtS5SGUuOjY/2l2EZ/mKdcd3m7pwzb1sv3HDuXquXnwbq+NNCpKcLO 9 | TNJsReTWrwSzfNj3eJapxzcn1S+XuJ9aEdsBj/uHAR3li8kubv9TAj5bUDeeoZJN 10 | n1F3ajC0uimb1aIlno8h7pDBLY2cpz6YUeR9QbWpP1wrleo1PndueuVIsue+txU2 11 | Md5pKOLFMAFvy4s2yQeGmhPZ/5APbWdnhfkslnYcBi36r4ODgje6s3IzkDe4H5Td 12 | gFTmPcLqOrn297sny6NM+C3gMBiNkApd7pcctQtUMxAKMMEmJLST85IPjs0CAwEA 13 | AQKCAgBQdVrmbfYYoR4B6qLsukHH99mR5FCEe2+4u3D2PA+HHsQbLM4FZ4Dgb40Z 14 | iq7/Xe5JkpzhUXTWRjGQKzCk5Vb6WsIFx0Xnf1O7YaA12VBQ5MF9xtoH4qSmizMj 15 | /pws34EAtbZrPPrQRAekAUkLfCBJep3e2cC35NACFsEJEnyTa6UF4g54vVUYa0HU 16 | xCa1pOqa5TBXv9iW0w8obaFzF+Th0y+Z1Pg3R46D5WGbc33YHs4BFZhzgPCYlqDX 17 | dvlRu9kYA1tbiPhBJ88THOfD2n2jPmIQtfp5lBDiCzrurFiHH7MuIvbcoWwmhjPE 18 | BbgdgJICe/ngc5kdWQhXvW+IABx+mKzDCw0GQHhrdDHedIxq4sGY+UzEwxfoY2xW 19 | WNT1sro+Ksw7FZZbebepgpNqevGRuWYzu2EoyaUK8CYihCShun/2YQK71r3b5aVm 20 | U35DL5nxJIdT0ahJ0jqXXcWx6byArMY1WK9L90gDQmT74eMVk6tmNfo+TtGRsCjy 21 | mOl02x6bQksdUtTI2s3rc1WPbnNdplfrJBjwJDcUEIbu11WxoPrHw/3PeodykVsj 22 | 8Ni5ebNuX+7t9iE+QurMpugOzP2jNUDuxcYrVF9brpktOpgv8IlDNLvzsi3myuhR 23 | m9V2uvU0C+gpDQzuph3/WzVdqi+8X3sbvNmeDWjjtftb3IKm4QKCAQEA0lA/p1qn 24 | JzmmdrE3VtGu72D8Cf/cyG6jwvlwX39kM4/hZV2n7lQsxB5gnhc5YiyXws+7Dn3p 25 | iMn1tw0/o5mNwNTpiirck3/BvW6czTvzelq9/ZcxJo79zuwehjSsuMKPbLvXn7Ba 26 | rI76HggE7SWHWA6nySsGLY80F0HkpOgLkCuIOx2oW1Zb5nfFZcWeuDndUvv2Thez 27 | 2j9UPtOUhoWDaw7cY44OclI7J+s/4uMzoUZXBxRk7CNq7xpYMnLmKvwTFqf58t9P 28 | Pbupktd6yYXBqBHuzmdVl6GagLD7mR2WbYJ8AjKhuEcRKMVfkydtYd3i9c+3hLW8 29 | JTvoOirgmtSNRQKCAQEAy01seeJbQmunXuzVS3AIiEL1bu7/UvScUCSjpNzKR3YV 30 | VmP7JaWWIlbQRnetoerUIvCJpiEcnrpOlxDphdeQa9dTNkk2ztb1Rj/osV5USHEl 31 | jEgHIf1FmmrM6ZzB/b7VJk5FUqzOZEeaObdmk9C2xtKrumwZ8D9cDveLCoDu87pI 32 | 61J4OT5iwvVxt7s4Du12nb6FQxNWfGWFWhw7FGSleK2LnJHJdiMIvdiGTxUgDjn5 33 | dn1+7XugigRvpUmv3DrwefWkE7DkRRPlu122fd6kK8nvkKajJw69R/SgZBon7kef 34 | fbb9fSnnfSkA4dIBpTtDtakbnEhG6o/Su8pW3Gs/6QKCAQAeHF0wsbry078wiSja 35 | JkU8go8zQ02x6J1Lofjjw1JuS3BC2gjcB3MtVQgSOlL96lKEEse+SGqyKfAjGCN/ 36 | YdG4xQL2xDI2b/kmDPsoKygt4WYIM6hW0+wkvwuTvWDpRvnP4Ij7lP02bXYD7LP/ 37 | 2/qnsdl15NIKndEgb0+0CID3UDQ9+n4LLa8UrRs2+fdCew5j/i0Ce0RFwAFoyVQf 38 | emgZYNRO8JzC42ES0wyfiFXxBigZnGLiqCN8PjJYbrjjeJmnCb+wdSZcOU0K+Azd 39 | Y2gZjw+4v3Sys/Fx8WTkRCcwYJkum18qCgq74p5PbDqt413GQcoNlxNr5UrXYSIt 40 | KLddAoIBAQC3AveHsRDd9fMxLJnF0xCbOUuflV4a20BrlNALdQZS1iXXIyHOfgVs 41 | 3CGZjdqsS6yz1zzSZDRTXvuoWf0eEzNbIPczgyznffJGTvm10Wil3dUjNyPUoR6r 42 | J0FXe1nWhpdyaDtXdWBGPX7EPikFH3mp+bPFmdKvxxmkD4sG5ZI1rZg+3nqDbXmS 43 | b0jzUIHiTjndPsjP3PSZ/vnQaGF2tjOPMwre4w4sXtVbsTMWtbmplN7Qn6BHQGcA 44 | V4X5kR/SbOxVnZ8aar7SwFqqFG5XWLkJAju6R4fPfSE/SSOpeTJA+hDFJpmCttpA 45 | fUzh/B6nE3acbaMBSL8uIFJf4oHW4mUhAoIBAQCrmCt2uZKWwmVcYnfhxmE97VuO 46 | WPJVUSBkdEQvi1AraoAMTcBweagpieW77EQYjwwHbcFmgZL4lfEF6ZBh1XvxM3J+ 47 | D0E3StvNtxaEHWHTZTVI6JVv6IYBKf+NFEklF51bbkpfYyaA9LHz0EiRPGIFe3FC 48 | wXPtCOpcZQLrY0MY4Z5FCWgu2zzt87xqzjZEJQnuntTvtwoZpck3J4ZfXsuPmRk4 49 | 4WwazE9J8NTyubtHzKDtxYB9z04iVT+LrUFhoekAXNc9ufqFHu56yTkBkZEfeWeh 50 | AHbGubrp9enuF+rH9E+4TqX1yO/q8h84m5KnaFMh6cyiVQM56SYps3Tc2pMV 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /config/jwt/private.pem-back: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: AES-256-CBC,E16106A85FA583DB7A5F16D4C1305A9C 4 | 5 | X1uf2ZDFheaMqMuwwQE80uDHVz1peLNT6xFu2W77DH5Y1kGgBFLLqaeP+4jDDWBY 6 | cuFzcAIpNZaOEuG5FrogyIZ0skCWuGyudIouHEW2/4x/MYgFvTSYtTUENPg+RUKX 7 | 6lbCNVTZ7MJWuEaBd7L+xxPEF/X90N6vy4NiZ/sWZW69N/uYxnjc8UWRpxs4plU8 8 | 4xBmDlRogdFgUbmjLG2WU9IehvJc+s9j+3P+LFM2IWPbcwaFCVh89f0enG/H7lP+ 9 | 17yzGImKRrMcP4NN5CptSvkp1O/ByE98M51c64HzwHBsgjP3GMcfqZy/OjLNv0SU 10 | nrEOBFY+wPbE/tAPbtJ+q0GNzOHDp7sPad+qkqDUJD9nKJPDUmHtDEXFb/bDlNqy 11 | CAd8hmEXnLWBQDc4OTPTY7bqPhv4r3iYhBa92iP07T7YfFolf5xI2scBj/RoY6e1 12 | ZAe/4l+exezrzsiBAHxgGAdElKGD8uJFHyM9Wsg10fvTHeAMkubXCyMuCv1D4cOu 13 | Mgb3ZfungG0+BWHbX8UCOCwIX+u5WSce+iJJjkVYPimFcZaYyXdqIbATwmdoKx6+ 14 | ZVu5fHpYwjMMOjeLOJ/LACPiae63uq2zIBA686AshZndZcAU+jk10XJZ+1JdP7n3 15 | HEao0em9CAtj5jhMMeMxSDIhobnr3p25WDJapuTkpvuuyXbC1geHYUXwrVNgmMNm 16 | hNHkP3+V1mADPsidyojSGQ6oWrRpUhQLdkvrsrKM6YSItGXjZEvflKwGqFxs05LI 17 | QFJClpQQUYnvrl0uBdhcCJF3TRFdBTNDX8hFeDs4jXbn+l/3Mk+dTDtwvzJzSJN1 18 | 21Y1I3c0l3ODCQfzciUdMBleNpQDuRz2X1dX9ptxA29COcohxtrnIfNy55iMrvVS 19 | Bjbr8IUgnnckT4NkB3IRAlVYLd6ZWfzbgnc+rQWllpW86DeNmG3mQCX0bU5I7beP 20 | Oj+4Tf9gT9nTHGhqR4zM8/th7FinnFo6sPw07TVs2qlVdZyKrJGZKNOFA5R+flYb 21 | dHxqJpE9ZvqHmLUEjzdTOiImlR4ARNr31P3Y+1ybABctZmZ3z3QmMhBgdu/8RmfM 22 | kAmouhy3/c/j+VduRwYtXQ71hoxeuGTagGiXiQHPM3rDx4qGOeKutWX3JIZUY4a7 23 | jlzZzetc+Ow0I1JIa9phWVYqfcYFCfSComRGG1oTifHkgXurqTW1Qfy2vdVHCGSS 24 | Xfdv1dNoSFyfNRtzUD9Ei9D5sxPaosL3BQekUgCG+TeVkcOoiTERylcFsLlOU0gZ 25 | B3Hz8f1Tb5Su3gCA/onBkitHh5r4IUvmYzIMLpc3Nm88Xkpzkhml3iNUVnodS1lX 26 | 9CiWTErtTufJwgJcZFqHXms5/fzNa4PqO3myh/1cK0ZbjR6qLt2w/ycn9fS5eQFT 27 | 0MBkFHzBHmlyB+IvYDEo6vL8CGtwBqBM3LC2+0ZSiIFBWoYCH9uVz8kv/YWKHEZg 28 | EnUjjqkp0voQYp0/5chmDxOHt0jdYem4KJC+qTDNzmhpmacrDx5e6Ch5XopVf15c 29 | vrkIoYJf9DMYsZE/3FD1Dr0ULB36ZYQliCrZXZpoN2NyvR6WIV2ZiE1HJFysYfS5 30 | 0b2U9ivrhLbwhnw6vakG49JSSNxpYzKjmIFq7gpyu2/VyyLU7CqAWE+3kwR4G/96 31 | dmTwloI+v6EVc464rYgofOuGFhNriz6Zn5J/SR4H8rlPaTmq2QdHVYWR1uJ9joD0 32 | CTgbnY8yMkm9txWnzI+4WOLsPjIj3pj0N9jf5qNGD8e95yY0eU9G71bbczXiome7 33 | qVhViFniW9NNp35e/QrXGfvsCobQYfFCnXSW7q/tfi67l8mFpSL9hnscPdH69qAs 34 | 6D86EnRNrs1LutKFfrlTFnlLUSQ1oLfmZZdg6AWosXC3YC2MiU2L0T9JK27kmf1V 35 | 2Edo/IJ3lnjEDiSUUdRXR81rPZr4QrWBvaIMB3K74lqZ5JkG8s9nm2bKbUfk1KRX 36 | 8FkmVw+pm7NYkh+/e2G65WYZHSOYjL+Q5DLacAJZrJvG4t+rMizEmLnnQNm4p/Fx 37 | 0fcmkDYddMVNU4IFsrD4gi37WpzDbMYAulc6yBbnMeXj6/e565i/puuInkFmYQiZ 38 | VrN5nWZa8vCzUKmXALb1aRBjDsPeXoLbCbcK+WgmqoFCweUoBWrvJyQpTZCg8pdL 39 | L2eG8cMoGJN0Yqpg86r15ZhcpqDmZYXNpNJK5GV8i/bAKFIVb4RkX4kEB0uJVSnm 40 | X/Odc8M/WzAZUF27XYF/rA6OIbUfQygenkCDKbtA38cletncacKu2eOBgESYDphX 41 | WhCkW5+8t19aTFV5sNwV1CZiKMImKL17liKCHjVQ22ZB2ywFf01jojHDW53v9w0Z 42 | TEpmVmYIlAcRwRRHCdIdO12DUZeFTnP6p+w8YIQ/tzIyWUKWaodTWioZ4G023YN3 43 | bLZmJP/x6pei3fhZIfY+1NyfbJYTcVWP/GIp9qMHzDTLJMAP1qd7BdmydPovIj4J 44 | WC8YbDeLZoId1vDH70NDnB2sjdTaseuXZLsPHt+xPYFBO9fUnWM+pW/WID5KeTjU 45 | W0dcwxm6A4Zn3/l9oPmyrefdD+dOlR93XwhbdXEOnxQjN4f9AbqBVqoabmi1Oxfp 46 | 6g7JUFooGo0YTswrpz9A7ldiSXG5f9kUggntbkwezuM25ddJ+FgnXTQYmWP6STBM 47 | jtLZcq6KH1t57KvC7QBJP+nAL4J41NnNnlL+GBst2quFhHhC8acTwASJ1Ck4cH0U 48 | puoFMoLcoGsuURbvqfcgP0oVDOtLDhDDZGxzJfe0RY24m5zQU1Js660SHqlyVQ26 49 | CtP9YWPMokWMqP12so2Rd/IR2phJ1v4xE7AQ3sg0VHs7FEoTNc682vO/m+HQik8d 50 | wTnOA9tc4beKzMXKMY+ztdAiK40gyb4vPPkmHKo+QqeZOQEUmTlloEGNAa629i17 51 | j5KZPoVYk3UVHEkAwGe4P6FsA3/SQaYI0CEBtd+aTmfKBJ3/Z/Y0u91su++3SWba 52 | YZJbqohHx7DTK727yq2q4PEHhfC89MB3mj5fKjKt7VFQNJEl14x1GIrbfAV29jF9 53 | g/nqWR+bB5CEOYg7KzccnvLj/hvW3UudwhX4/PoUJ2zWR4Z/R3NS38gNeFN+4S/P 54 | -----END RSA PRIVATE KEY----- 55 | -------------------------------------------------------------------------------- /config/jwt/public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApwU9utb3UgNg3eygg8rT 3 | xNHVWJpYLjghT9saV7IjZV6Mow6pgZCvmgdC+oqEbouhDymX3g9HLyTvEFhb68bZ 4 | S27jvtlTtmIHLIDkiBOTZMaNjMcuRCKD10sG/cShIc13LOdZvyyS8JvAVKmT6swP 5 | 11mrPlQdwY8kaESXsj/1aMY+yj1rENLwRnOI7pMBsWxwlGuMNxu59mFWNzoIdIRN 6 | LShu98IQ6rJTNQWhq4hLJfp0ZDhqp5X4qnie2a9TIMsABfBxQOFjTvBT5nDx+FCl 7 | I8itQm9bvxYI/D8gR6zUo3DiP3GWNeSM3TEyqCugPv52p7hrVI+7Pu7QrP1HFZ9+ 8 | hBgiHiMBrjMcq/UKjqb3gmGMT/I0bMX3yAk7ErRtS5SGUuOjY/2l2EZ/mKdcd3m7 9 | pwzb1sv3HDuXquXnwbq+NNCpKcLOTNJsReTWrwSzfNj3eJapxzcn1S+XuJ9aEdsB 10 | j/uHAR3li8kubv9TAj5bUDeeoZJNn1F3ajC0uimb1aIlno8h7pDBLY2cpz6YUeR9 11 | QbWpP1wrleo1PndueuVIsue+txU2Md5pKOLFMAFvy4s2yQeGmhPZ/5APbWdnhfks 12 | lnYcBi36r4ODgje6s3IzkDe4H5TdgFTmPcLqOrn297sny6NM+C3gMBiNkApd7pcc 13 | tQtUMxAKMMEmJLST85IPjs0CAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /config/packages/dev/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | test: ~ 3 | session: 4 | storage_id: session.storage.mock_file 5 | -------------------------------------------------------------------------------- /config/packages/dev/hautelook_alice.yaml: -------------------------------------------------------------------------------- 1 | hautelook_alice: 2 | fixtures_path: fixtures 3 | -------------------------------------------------------------------------------- /config/packages/dev/jms_serializer.yaml: -------------------------------------------------------------------------------- 1 | jms_serializer: 2 | visitors: 3 | json: 4 | options: 5 | - JSON_PRETTY_PRINT 6 | - JSON_UNESCAPED_SLASHES 7 | - JSON_PRESERVE_ZERO_FRACTION 8 | -------------------------------------------------------------------------------- /config/packages/dev/nelmio_alice.yaml: -------------------------------------------------------------------------------- 1 | nelmio_alice: 2 | functions_blacklist: 3 | - 'current' 4 | -------------------------------------------------------------------------------- /config/packages/dev/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: true 4 | -------------------------------------------------------------------------------- /config/packages/dev/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler: 2 | toolbar: true 3 | intercept_redirects: false 4 | 5 | framework: 6 | profiler: { only_exceptions: false } 7 | -------------------------------------------------------------------------------- /config/packages/doctrine.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | # Adds a fallback DATABASE_URL if the env var is not set. 3 | # This allows you to run cache:warmup even if your 4 | # environment variables are not available yet. 5 | # You should not need to change this value. 6 | env(DATABASE_URL): '' 7 | 8 | doctrine: 9 | dbal: 10 | # configure these for your database server 11 | driver: 'pdo_sqlite' 12 | charset: utf8 13 | default_table_options: 14 | charset: utf8 15 | collate: utf8_bin 16 | 17 | url: '%env(resolve:DATABASE_URL)%' 18 | orm: 19 | auto_generate_proxy_classes: '%kernel.debug%' 20 | naming_strategy: doctrine.orm.naming_strategy.underscore 21 | auto_mapping: true 22 | mappings: 23 | App: 24 | is_bundle: false 25 | type: annotation 26 | dir: '%kernel.project_dir%/src/Entity' 27 | prefix: 'App\Entity' 28 | alias: App 29 | -------------------------------------------------------------------------------- /config/packages/doctrine_migrations.yaml: -------------------------------------------------------------------------------- 1 | doctrine_migrations: 2 | dir_name: '%kernel.project_dir%/src/Migrations' 3 | # namespace is arbitrary but should be different from App\Migrations 4 | # as migrations classes should NOT be autoloaded 5 | namespace: DoctrineMigrations 6 | -------------------------------------------------------------------------------- /config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | secret: '%env(APP_SECRET)%' 3 | default_locale: en 4 | csrf_protection: true 5 | http_method_override: true 6 | 7 | # Enables session support. Note that the session will ONLY be started if you read or write from it. 8 | # Remove or comment this section to explicitly disable session support. 9 | session: 10 | handler_id: ~ 11 | 12 | #esi: true 13 | #fragments: true 14 | php_errors: 15 | log: true 16 | 17 | cache: 18 | # Put the unique name of your app here: the prefix seed 19 | # is used to compute stable namespaces for cache keys. 20 | #prefix_seed: your_vendor_name/app_name 21 | 22 | # The app cache caches to the filesystem by default. 23 | # Other options include: 24 | 25 | # Redis 26 | #app: cache.adapter.redis 27 | #default_redis_provider: redis://localhost 28 | 29 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) 30 | #app: cache.adapter.apcu 31 | -------------------------------------------------------------------------------- /config/packages/jms_serializer.yaml: -------------------------------------------------------------------------------- 1 | jms_serializer: 2 | property_naming: 3 | id: 'jms_serializer.identical_property_naming_strategy' 4 | 5 | # metadata: 6 | # auto_detection: false 7 | # directories: 8 | # any-name: 9 | # namespace_prefix: "My\\FooBundle" 10 | # path: "@MyFooBundle/Resources/config/serializer" 11 | # another-name: 12 | # namespace_prefix: "My\\BarBundle" 13 | # path: "@MyBarBundle/Resources/config/serializer" 14 | -------------------------------------------------------------------------------- /config/packages/lexik_jwt_authentication.yaml: -------------------------------------------------------------------------------- 1 | lexik_jwt_authentication: 2 | secret_key: '%env(resolve:JWT_SECRET_KEY)%' 3 | public_key: '%env(resolve:JWT_PUBLIC_KEY)%' 4 | pass_phrase: '%env(JWT_PASSPHRASE)%' 5 | -------------------------------------------------------------------------------- /config/packages/nelmio_api_doc.yaml: -------------------------------------------------------------------------------- 1 | nelmio_api_doc: 2 | documentation: 3 | models: { use_jms: false } 4 | routes: 5 | path_patterns: 6 | - ^/(?!/$) 7 | schemes: [http, https] 8 | info: 9 | title: Symfony Rest API Demo 10 | version: beta 11 | securityDefinitions: 12 | Bearer: 13 | type: apiKey 14 | description: 'Value: Bearer {jwt}' 15 | name: Authorization 16 | in: header 17 | security: 18 | - Bearer: [] 19 | areas: # to filter documented areas 20 | path_patterns: 21 | - ^/(?!/$) 22 | -------------------------------------------------------------------------------- /config/packages/prod/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | url: '%env(DATABASE_URL)%' 4 | orm: 5 | auto_generate_proxy_classes: '%kernel.debug%' 6 | naming_strategy: doctrine.orm.naming_strategy.underscore 7 | auto_mapping: true 8 | mappings: 9 | App: 10 | is_bundle: false 11 | type: annotation 12 | dir: '%kernel.project_dir%/src/Entity' 13 | prefix: 'App\Entity\' 14 | alias: App 15 | metadata_cache_driver: 16 | type: service 17 | id: doctrine.system_cache_provider 18 | query_cache_driver: 19 | type: service 20 | id: doctrine.system_cache_provider 21 | result_cache_driver: 22 | type: service 23 | id: doctrine.result_cache_provider 24 | 25 | services: 26 | doctrine.result_cache_provider: 27 | class: Symfony\Component\Cache\DoctrineProvider 28 | public: false 29 | arguments: 30 | - '@doctrine.result_cache_pool' 31 | doctrine.system_cache_provider: 32 | class: Symfony\Component\Cache\DoctrineProvider 33 | public: false 34 | arguments: 35 | - '@doctrine.system_cache_pool' 36 | 37 | framework: 38 | cache: 39 | pools: 40 | doctrine.result_cache_pool: 41 | adapter: cache.app 42 | doctrine.system_cache_pool: 43 | adapter: cache.system 44 | -------------------------------------------------------------------------------- /config/packages/prod/jms_serializer.yaml: -------------------------------------------------------------------------------- 1 | jms_serializer: 2 | visitors: 3 | json: 4 | options: 5 | - JSON_UNESCAPED_SLASHES 6 | - JSON_PRESERVE_ZERO_FRACTION 7 | -------------------------------------------------------------------------------- /config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: ~ 4 | -------------------------------------------------------------------------------- /config/packages/security.yaml: -------------------------------------------------------------------------------- 1 | security: 2 | # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers 3 | providers: 4 | webservice: 5 | id: App\Security\UserProvider 6 | 7 | encoders: 8 | App\Entity\User: 9 | algorithm: bcrypt 10 | cost: 12 11 | 12 | firewalls: 13 | dev: 14 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 15 | security: false 16 | 17 | login: 18 | pattern: ^/login 19 | stateless: true 20 | anonymous: true 21 | json_login: 22 | check_path: /login_check 23 | success_handler: lexik_jwt_authentication.handler.authentication_success 24 | failure_handler: lexik_jwt_authentication.handler.authentication_failure 25 | 26 | api: 27 | pattern: ^/ 28 | anonymous: true 29 | stateless: true 30 | guard: 31 | authenticators: 32 | - lexik_jwt_authentication.jwt_token_authenticator 33 | 34 | # Easy way to control access for large sections of your site 35 | # Note: Only the *first* access control that matches will be used 36 | access_control: 37 | - { path: ^/(.+), roles: IS_AUTHENTICATED_ANONYMOUSLY } 38 | - { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY } 39 | - { path: ^/$, roles: IS_AUTHENTICATED_ANONYMOUSLY } 40 | -------------------------------------------------------------------------------- /config/packages/security_checker.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | SensioLabs\Security\SecurityChecker: 3 | public: false 4 | 5 | SensioLabs\Security\Command\SecurityCheckerCommand: 6 | arguments: ['@SensioLabs\Security\SecurityChecker'] 7 | public: false 8 | tags: 9 | - { name: console.command, command: 'security:check' } 10 | -------------------------------------------------------------------------------- /config/packages/sensio_framework_extra.yaml: -------------------------------------------------------------------------------- 1 | sensio_framework_extra: 2 | router: 3 | annotations: false 4 | -------------------------------------------------------------------------------- /config/packages/stof_doctrine_extensions.yaml: -------------------------------------------------------------------------------- 1 | # Read the documentation: https://symfony.com/doc/current/bundles/StofDoctrineExtensionsBundle/index.html 2 | # See the official DoctrineExtensions documentation for more details: https://github.com/Atlantic18/DoctrineExtensions/tree/master/doc/ 3 | stof_doctrine_extensions: 4 | default_locale: en_US 5 | orm: 6 | default: 7 | timestampable: true 8 | -------------------------------------------------------------------------------- /config/packages/test/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | test: ~ 3 | session: 4 | storage_id: session.storage.mock_file 5 | -------------------------------------------------------------------------------- /config/packages/test/hautelook_alice.yaml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: ../dev/hautelook_alice.yaml } 3 | -------------------------------------------------------------------------------- /config/packages/test/nelmio_alice.yaml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: ../dev/nelmio_alice.yaml } 3 | -------------------------------------------------------------------------------- /config/packages/test/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: true 4 | -------------------------------------------------------------------------------- /config/packages/test/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler: 2 | toolbar: false 3 | intercept_redirects: false 4 | 5 | framework: 6 | profiler: { collect: false } 7 | -------------------------------------------------------------------------------- /config/packages/translation.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | default_locale: '%locale%' 3 | translator: 4 | default_path: '%kernel.project_dir%/translations' 5 | fallbacks: 6 | - '%locale%' 7 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | default_path: '%kernel.project_dir%/templates' 3 | debug: '%kernel.debug%' 4 | strict_variables: '%kernel.debug%' 5 | -------------------------------------------------------------------------------- /config/packages/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | email_validation_mode: html5 4 | -------------------------------------------------------------------------------- /config/routes.yaml: -------------------------------------------------------------------------------- 1 | app.swagger_ui: 2 | path: / 3 | methods: GET 4 | defaults: { _controller: nelmio_api_doc.controller.swagger_ui } 5 | 6 | api.login_check: 7 | path: /login_check 8 | 9 | _errors: 10 | resource: '@TwigBundle/Resources/config/routing/errors.xml' 11 | prefix: /_error 12 | 13 | 14 | -------------------------------------------------------------------------------- /config/routes/annotations.yaml: -------------------------------------------------------------------------------- 1 | controllers: 2 | resource: ../../src/Controller/ 3 | type: annotation 4 | -------------------------------------------------------------------------------- /config/routes/dev/twig.yaml: -------------------------------------------------------------------------------- 1 | _errors: 2 | resource: '@TwigBundle/Resources/config/routing/errors.xml' 3 | prefix: /_error 4 | -------------------------------------------------------------------------------- /config/routes/dev/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler_wdt: 2 | resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' 3 | prefix: /_wdt 4 | 5 | web_profiler_profiler: 6 | resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' 7 | prefix: /_profiler 8 | -------------------------------------------------------------------------------- /config/routes/nelmio_api_doc.yaml: -------------------------------------------------------------------------------- 1 | # Expose your documentation as JSON swagger compliant 2 | app.swagger: 3 | path: /api/doc.json 4 | methods: GET 5 | defaults: { _controller: nelmio_api_doc.controller.swagger } 6 | 7 | ## Requires the Asset component and the Twig bundle 8 | # $ composer require twig asset 9 | #app.swagger_ui: 10 | # path: / 11 | # methods: GET 12 | # defaults: { _controller: nelmio_api_doc.controller.swagger_ui } 13 | -------------------------------------------------------------------------------- /config/services.yaml: -------------------------------------------------------------------------------- 1 | # This file is the entry point to configure your own services. 2 | # Files in the packages/ subdirectory configure your dependencies. 3 | framework: 4 | session: 5 | handler_id: session.handler.native_file 6 | save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%' 7 | 8 | # Put parameters here that don't need to change on each machine where the app is deployed 9 | # https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration 10 | parameters: 11 | locale: 'en' 12 | symfony: 13 | container_xml_path: srcDevDebugProjectContainer.xml 14 | 15 | services: 16 | # default configuration for services in *this* file 17 | _defaults: 18 | autowire: true # Automatically injects dependencies in your services. 19 | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. 20 | public: false # Allows optimizing the container by removing unused services; this also means 21 | # fetching services directly from the container via $container->get() won't work. 22 | # The best practice is to be explicit about your dependencies anyway. 23 | 24 | # makes classes in src/ available to be used as services 25 | # this creates a service per class whose id is the fully-qualified class name 26 | App\: 27 | resource: '../src/*' 28 | exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}' 29 | 30 | # controllers are imported separately to make sure services can be injected 31 | # as action arguments even if you don't extend any base controller class 32 | App\Controller\: 33 | resource: '../src/Controller' 34 | tags: ['controller.service_arguments'] 35 | calls: 36 | - [setContainer, ['@service_container']] 37 | 38 | # make doctrine Inflector available to be used as service 39 | Doctrine\Common\Inflector\Inflector: 40 | 41 | # add more service definitions when explicit configuration is needed 42 | # please note that last definitions always *replace* previous ones 43 | 44 | # App\EventListener\ExceptionListener: 45 | # tags: 46 | # - { name: kernel.event_listener, event: kernel.exception } 47 | 48 | App\EventSubscriber\UserSubscriber: 49 | tags: 50 | - { name: doctrine.event_listener, event: prePersist } 51 | - { name: doctrine.event_listener, event: postUpdate } 52 | - { name: doctrine.event_listener, event: onFlush } 53 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | /** HEADER **/ 2 | 3 | header:before { 4 | content:""; 5 | background-color: #265a8e; 6 | height:70px; 7 | width:100%; 8 | text-align:center; 9 | position:fixed; 10 | top:0; 11 | z-index:100; 12 | box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); 13 | } 14 | 15 | header #logo { 16 | position:fixed; 17 | top : 35px; 18 | right:40px; 19 | z-index:102; 20 | transform:translateY(-50%); 21 | } 22 | 23 | header #logo img { 24 | height:48px; 25 | background-color: #265a8e !important; 26 | } 27 | 28 | #swagger-ui.api-platform .info .title { 29 | color:#265a8e; 30 | } 31 | 32 | #swagger-ui.api-platform .info { 33 | width: 100%; 34 | max-width: 1460px; 35 | padding: 0px 50px; 36 | margin: 0px auto; 37 | } 38 | 39 | /** METHODS BLOCS **/ 40 | 41 | #swagger-ui.api-platform .opblock.opblock-get .opblock-summary-method { 42 | background-color:#265a8e; 43 | } 44 | 45 | #swagger-ui.api-platform .opblock.opblock-get .opblock-summary { 46 | border-color:#265a8e; 47 | } 48 | 49 | /** BUTTONS **/ 50 | 51 | #swagger-ui.api-platform .btn.execute { 52 | background-color:#265a8e; 53 | border-color:#265a8e; 54 | animation:none; 55 | transition:all ease 0.3s; 56 | } 57 | 58 | #swagger-ui.api-platform .btn.execute:hover { 59 | background-color:#265a8e; 60 | border-color:#265a8e; 61 | } -------------------------------------------------------------------------------- /fixtures/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/fixtures/.gitignore -------------------------------------------------------------------------------- /fixtures/data.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - data/books.yml 3 | - data/movies.yml 4 | - data/reviews.yml 5 | - data/users.yml 6 | -------------------------------------------------------------------------------- /fixtures/data/books.yml: -------------------------------------------------------------------------------- 1 | App\Entity\Book: 2 | book_{1..100}: 3 | isbn (unique): 4 | title: 5 | description: 6 | author: 7 | publicationDate: 8 | -------------------------------------------------------------------------------- /fixtures/data/movies.yml: -------------------------------------------------------------------------------- 1 | App\Entity\Movie: 2 | movie_{1..100}: 3 | duration: 4 | title: 5 | description: 6 | director: 7 | publicationDate: 8 | -------------------------------------------------------------------------------- /fixtures/data/reviews.yml: -------------------------------------------------------------------------------- 1 | App\Entity\Review: 2 | review_{1..1000}: 3 | body: 4 | rating: 5 | author: '@user*' 6 | publicationDate: 7 | book: '@book*' 8 | movie: '@movie*' 9 | -------------------------------------------------------------------------------- /fixtures/data/users.yml: -------------------------------------------------------------------------------- 1 | App\Entity\User: 2 | user_developer: 3 | fullName: 'Symfony Developer' 4 | email (unique): 'developer@symfony.local' 5 | plainPassword: 'developer' 6 | books (unique): 'x @book*' 7 | movies (unique): 'x @movie*' 8 | roles: ["ROLE_ADMIN"] 9 | 10 | user_{1..10}: 11 | fullName: 12 | email (unique): 13 | plainPassword: "changeMe" 14 | books (unique): 'x @book*' 15 | movies (unique): 'x @movie*' 16 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The Symfony coding standard. 5 | 6 | 7 | */Resources/* 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 0 56 | 57 | 58 | 59 | 60 | 0 61 | 62 | 63 | 64 | 65 | 0 66 | 67 | 68 | 0 69 | 70 | 71 | 0 72 | 73 | 74 | 75 | 0 76 | 77 | 78 | 79 | error 80 | 81 | 82 | 83 | error 84 | 85 | 86 | 87 | error 88 | 89 | 90 | 91 | error 92 | 93 | 94 | 95 | 0 96 | 97 | 98 | 99 | There should always be a description, followed by a blank line, before the tags of a class comment. 100 | 101 | 102 | bin/ 103 | config/ 104 | public/ 105 | src/ 106 | tests/ 107 | 108 | 109 | -------------------------------------------------------------------------------- /phpmd.dist.xml: -------------------------------------------------------------------------------- 1 | 7 | mess detection 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | tests/ 35 | 36 | 37 | 38 | 39 | ./src 40 | 41 | ./src/Kernel.php 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /public/css/roboto.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Roboto'; 3 | font-style: normal; 4 | font-weight: 300; 5 | src: local('Roboto Light'), local('Roboto-Light'), url(../fonts/roboto-light.ttf) format('truetype'); 6 | } 7 | @font-face { 8 | font-family: 'Roboto'; 9 | font-style: normal; 10 | font-weight: 700; 11 | src: local('Roboto Bold'), local('Roboto-Bold'), url(../fonts/roboto-bold.ttf) format('truetype'); 12 | } 13 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicon.ico -------------------------------------------------------------------------------- /public/favicons/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/android-icon-144x144.png -------------------------------------------------------------------------------- /public/favicons/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/android-icon-192x192.png -------------------------------------------------------------------------------- /public/favicons/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/android-icon-36x36.png -------------------------------------------------------------------------------- /public/favicons/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/android-icon-48x48.png -------------------------------------------------------------------------------- /public/favicons/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/android-icon-72x72.png -------------------------------------------------------------------------------- /public/favicons/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/android-icon-96x96.png -------------------------------------------------------------------------------- /public/favicons/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/apple-icon-114x114.png -------------------------------------------------------------------------------- /public/favicons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/apple-icon-120x120.png -------------------------------------------------------------------------------- /public/favicons/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/apple-icon-144x144.png -------------------------------------------------------------------------------- /public/favicons/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/apple-icon-152x152.png -------------------------------------------------------------------------------- /public/favicons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/apple-icon-180x180.png -------------------------------------------------------------------------------- /public/favicons/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/apple-icon-57x57.png -------------------------------------------------------------------------------- /public/favicons/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/apple-icon-60x60.png -------------------------------------------------------------------------------- /public/favicons/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/apple-icon-72x72.png -------------------------------------------------------------------------------- /public/favicons/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/apple-icon-76x76.png -------------------------------------------------------------------------------- /public/favicons/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/apple-icon-precomposed.png -------------------------------------------------------------------------------- /public/favicons/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/apple-icon.png -------------------------------------------------------------------------------- /public/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff 3 | -------------------------------------------------------------------------------- /public/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/favicon-96x96.png -------------------------------------------------------------------------------- /public/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/favicon.ico -------------------------------------------------------------------------------- /public/favicons/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App", 3 | "icons": [ 4 | { 5 | "src": "\/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "\/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "\/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "\/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "\/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "\/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /public/favicons/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/ms-icon-144x144.png -------------------------------------------------------------------------------- /public/favicons/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/ms-icon-150x150.png -------------------------------------------------------------------------------- /public/favicons/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/ms-icon-310x310.png -------------------------------------------------------------------------------- /public/favicons/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/favicons/ms-icon-70x70.png -------------------------------------------------------------------------------- /public/images/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/public/images/github.png -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | load(__DIR__.'/../.env'); 18 | } 19 | 20 | $env = $_SERVER['APP_ENV'] ?? 'dev'; 21 | $debug = $_SERVER['APP_DEBUG'] ?? ('prod' !== $env); 22 | 23 | if ($debug) { 24 | umask(0000); 25 | 26 | Debug::enable(); 27 | } 28 | 29 | if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? false) { 30 | Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST); 31 | } 32 | 33 | if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? false) { 34 | Request::setTrustedHosts(explode(',', $trustedHosts)); 35 | } 36 | 37 | $kernel = new Kernel($env, $debug); 38 | $request = Request::createFromGlobals(); 39 | $response = $kernel->handle($request); 40 | $response->send(); 41 | $kernel->terminate($request, $response); 42 | -------------------------------------------------------------------------------- /public/js/clusterize.min.js: -------------------------------------------------------------------------------- 1 | /*! Clusterize.js - v0.17.6 - 2017-03-05 2 | * http://NeXTs.github.com/Clusterize.js/ 3 | * Copyright (c) 2015 Denis Lukov; Licensed GPLv3 */ 4 | 5 | ;(function(q,n){"undefined"!=typeof module?module.exports=n():"function"==typeof define&&"object"==typeof define.amd?define(n):this[q]=n()})("Clusterize",function(){function q(b,a,c){return a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent("on"+b,c)}function n(b,a,c){return a.removeEventListener?a.removeEventListener(b,c,!1):a.detachEvent("on"+b,c)}function r(b){return"[object Array]"===Object.prototype.toString.call(b)}function m(b,a){return window.getComputedStyle?window.getComputedStyle(a)[b]: 6 | a.currentStyle[b]}var l=function(){for(var b=3,a=document.createElement("b"),c=a.all||[];a.innerHTML="\x3c!--[if gt IE "+ ++b+"]>=l&&!c.tag&&(c.tag=b[0].match(/<([^>\s/]*)/)[1].toLowerCase()),1>=this.content_elem.children.length&&(a.data=this.html(b[0]+b[0]+b[0])),c.tag||(c.tag=this.content_elem.children[0].tagName.toLowerCase()), 11 | this.getRowsHeight(b))},getRowsHeight:function(b){var a=this.options,c=a.item_height;a.cluster_height=0;if(b.length){b=this.content_elem.children;var d=b[Math.floor(b.length/2)];a.item_height=d.offsetHeight;"tr"==a.tag&&"collapse"!=m("borderCollapse",this.content_elem)&&(a.item_height+=parseInt(m("borderSpacing",this.content_elem),10)||0);"tr"!=a.tag&&(b=parseInt(m("marginTop",d),10)||0,d=parseInt(m("marginBottom",d),10)||0,a.item_height+=Math.max(b,d));a.block_height=a.item_height*a.rows_in_block; 12 | a.rows_in_cluster=a.blocks_in_cluster*a.rows_in_block;a.cluster_height=a.blocks_in_cluster*a.block_height;return c!=a.item_height}},getClusterNum:function(){this.options.scroll_top=this.scroll_elem.scrollTop;return Math.floor(this.options.scroll_top/(this.options.cluster_height-this.options.block_height))||0},generateEmptyRow:function(){var b=this.options;if(!b.tag||!b.show_no_data_row)return[];var a=document.createElement(b.tag),c=document.createTextNode(b.no_data_text),d;a.className=b.no_data_class; 13 | "tr"==b.tag&&(d=document.createElement("td"),d.colSpan=100,d.appendChild(c));a.appendChild(d||c);return[a.outerHTML]},generate:function(b,a){var c=this.options,d=b.length;if(de&&g++;f=l&&"tr"==this.options.tag){var c=document.createElement("div");for(c.innerHTML=""+b+"
";b=a.lastChild;)a.removeChild(b);for(c=this.getChildNodes(c.firstChild.firstChild);c.length;)a.appendChild(c.shift())}else a.innerHTML=b},getChildNodes:function(b){b=b.children;for(var a=[],c=0,d=b.length;c 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Controller; 13 | 14 | use App\Entity\Book; 15 | use App\Exception\ApiException; 16 | use App\Form\BookType; 17 | use App\Form\Filter\BookFilter; 18 | use App\Interfaces\ControllerInterface; 19 | use Nelmio\ApiDocBundle\Annotation\Model; 20 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; 21 | use Swagger\Annotations as SWG; 22 | use Symfony\Component\HttpFoundation\JsonResponse; 23 | use Symfony\Component\HttpFoundation\Request; 24 | use Symfony\Component\HttpFoundation\Response; 25 | use Symfony\Component\Routing\Annotation\Route; 26 | 27 | /** 28 | * @Route(path="/books") 29 | */ 30 | class BookController extends AbstractController implements ControllerInterface 31 | { 32 | /** 33 | * BookController constructor. 34 | */ 35 | public function __construct() 36 | { 37 | parent::__construct(Book::class); 38 | } 39 | 40 | /** 41 | * Get all Books. 42 | * 43 | * @Route(name="api_book_list", methods={Request::METHOD_GET}) 44 | * 45 | * @SWG\Tag(name="Book") 46 | * @SWG\Response( 47 | * response=200, 48 | * description="Returns the list of books.yml", 49 | * @SWG\Schema( 50 | * type="array", 51 | * @SWG\Items(ref=@Model(type=Book::class)) 52 | * ) 53 | * ) 54 | * 55 | * @param Request $request 56 | * 57 | * @return JsonResponse 58 | */ 59 | public function listAction(Request $request): JsonResponse 60 | { 61 | return $this->createCollectionResponse( 62 | $this->handleFilterForm( 63 | $request, 64 | BookFilter::class 65 | ) 66 | ); 67 | } 68 | 69 | /** 70 | * Show single Books. 71 | * 72 | * @Route(path="/{book}", name="api_book_show", methods={Request::METHOD_GET}) 73 | * 74 | * @SWG\Tag(name="Book") 75 | * @SWG\Response( 76 | * response=200, 77 | * description="Returns book of given identifier.", 78 | * @SWG\Schema( 79 | * type="object", 80 | * @SWG\Items(ref=@Model(type=Book::class)) 81 | * ) 82 | * ) 83 | * 84 | * @param Book|null $book 85 | * 86 | * @return JsonResponse 87 | */ 88 | public function showAction(Book $book = null): JsonResponse 89 | { 90 | if (false === !!$book) { 91 | return $this->createNotFoundResponse(); 92 | } 93 | 94 | return $this->createResourceResponse($book); 95 | } 96 | 97 | /** 98 | * Add new Book. 99 | * 100 | * @Route(name="api_book_create", methods={Request::METHOD_POST}) 101 | * 102 | * @SWG\Tag(name="Book") 103 | * @SWG\Response( 104 | * response=201, 105 | * description="Creates new Book and returns the created object.", 106 | * @SWG\Schema( 107 | * type="object", 108 | * @SWG\Items(ref=@Model(type=Book::class)) 109 | * ) 110 | * ) 111 | * 112 | * @param Request $request 113 | * @param Book|null $book 114 | * 115 | * @return JsonResponse 116 | * 117 | * @Security("is_granted('CAN_CREATE_BOOK', book)") 118 | */ 119 | public function createAction(Request $request, Book $book = null): JsonResponse 120 | { 121 | if (false === !!$book) { 122 | $book = new Book(); 123 | } 124 | 125 | $form = $this->getForm( 126 | BookType::class, 127 | $book, 128 | [ 129 | 'method' => $request->getMethod(), 130 | ] 131 | ); 132 | 133 | try { 134 | $this->formHandler->process($request, $form); 135 | } catch (ApiException $e) { 136 | return new JsonResponse($e->getData(), Response::HTTP_BAD_REQUEST); 137 | } 138 | 139 | return $this->createResourceResponse($book, Response::HTTP_CREATED); 140 | } 141 | 142 | /** 143 | * Edit existing Book. 144 | * 145 | * @Route(path="/{book}", name="api_book_edit", methods={Request::METHOD_PATCH}) 146 | * 147 | * @SWG\Tag(name="Book") 148 | * @SWG\Response( 149 | * response=200, 150 | * description="Updates Book of given identifier and returns the updated object.", 151 | * @SWG\Schema( 152 | * type="object", 153 | * @SWG\Items(ref=@Model(type=Book::class)) 154 | * ) 155 | * ) 156 | * 157 | * @param Request $request 158 | * @param Book|null $book 159 | * 160 | * @return JsonResponse 161 | * 162 | * @Security("is_granted('CAN_UPDATE_BOOK', book)") 163 | */ 164 | public function updateAction(Request $request, Book $book = null): JsonResponse 165 | { 166 | if (false === !!$book) { 167 | return $this->createNotFoundResponse(); 168 | } 169 | 170 | $form = $this->getForm( 171 | BookType::class, 172 | $book, 173 | [ 174 | 'method' => $request->getMethod(), 175 | ] 176 | ); 177 | 178 | try { 179 | $this->formHandler->process($request, $form); 180 | } catch (ApiException $e) { 181 | return new JsonResponse($e->getMessage(), Response::HTTP_BAD_REQUEST); 182 | } 183 | 184 | return $this->createResourceResponse($book); 185 | } 186 | 187 | /** 188 | * Delete Book. 189 | * 190 | * @Route(path="/{book}", name="api_book_delete", methods={Request::METHOD_DELETE}) 191 | * 192 | * @SWG\Tag(name="Book") 193 | * @SWG\Response( 194 | * response=200, 195 | * description="Delete Book of given identifier and returns the empty object.", 196 | * @SWG\Schema( 197 | * type="object", 198 | * @SWG\Items(ref=@Model(type=Book::class)) 199 | * ) 200 | * ) 201 | * 202 | * @param Book|null $book 203 | * 204 | * @return JsonResponse 205 | * 206 | * @Security("is_granted('CAN_DELETE_BOOK', book)") 207 | */ 208 | public function deleteAction(Book $book = null): JsonResponse 209 | { 210 | if (false === !!$book) { 211 | return $this->createNotFoundResponse(); 212 | } 213 | 214 | try { 215 | $this->entityManager->remove($book); 216 | $this->entityManager->flush(); 217 | } catch (\Exception $exception) { 218 | return $this->createGenericErrorResponse($exception); 219 | } 220 | 221 | return $this->createSuccessfulApiResponse(self::DELETED); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/Controller/MovieController.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Controller; 13 | 14 | use App\Entity\Movie; 15 | use App\Exception\ApiException; 16 | use App\Form\Filter\MovieFilter; 17 | use App\Form\MovieType; 18 | use App\Interfaces\ControllerInterface; 19 | use Nelmio\ApiDocBundle\Annotation\Model; 20 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; 21 | use Swagger\Annotations as SWG; 22 | use Symfony\Component\HttpFoundation\JsonResponse; 23 | use Symfony\Component\HttpFoundation\Request; 24 | use Symfony\Component\HttpFoundation\Response; 25 | use Symfony\Component\Routing\Annotation\Route; 26 | 27 | /** 28 | * @Route(path="/movies") 29 | */ 30 | class MovieController extends AbstractController implements ControllerInterface 31 | { 32 | /** 33 | * MovieController constructor. 34 | */ 35 | public function __construct() 36 | { 37 | parent::__construct(Movie::class); 38 | } 39 | 40 | /** 41 | * Get all Movies. 42 | * 43 | * @Route(name="api_movie_list", methods={Request::METHOD_GET}) 44 | * 45 | * @SWG\Tag(name="Movie") 46 | * @SWG\Response( 47 | * response=200, 48 | * description="Returns the list of movies", 49 | * @SWG\Schema( 50 | * type="array", 51 | * @SWG\Items(ref=@Model(type=Movie::class)) 52 | * ) 53 | * ) 54 | * 55 | * @param Request $request 56 | * 57 | * @return JsonResponse 58 | */ 59 | public function listAction(Request $request): JsonResponse 60 | { 61 | return $this->createCollectionResponse( 62 | $this->handleFilterForm( 63 | $request, 64 | MovieFilter::class 65 | ) 66 | ); 67 | } 68 | 69 | /** 70 | * Show single Movies. 71 | * 72 | * @Route(path="/{movie}", name="api_movie_show", methods={Request::METHOD_GET}) 73 | * 74 | * @SWG\Tag(name="Movie") 75 | * @SWG\Response( 76 | * response=200, 77 | * description="Returns movie of given identifier.", 78 | * @SWG\Schema( 79 | * type="object", 80 | * title="movie", 81 | * @SWG\Items(ref=@Model(type=Movie::class)) 82 | * ) 83 | * ) 84 | * 85 | * @param Movie|null $movie 86 | * 87 | * @return JsonResponse 88 | */ 89 | public function showAction(Movie $movie = null): JsonResponse 90 | { 91 | if (false === !!$movie) { 92 | return $this->createNotFoundResponse(); 93 | } 94 | 95 | return $this->createResourceResponse($movie); 96 | } 97 | 98 | /** 99 | * Add new Movie. 100 | * 101 | * @Route(name="api_movie_create", methods={Request::METHOD_POST}) 102 | * 103 | * @SWG\Tag(name="Movie") 104 | * @SWG\Response( 105 | * response=200, 106 | * description="Updates Movie of given identifier and returns the updated object.", 107 | * @SWG\Schema( 108 | * type="object", 109 | * @SWG\Items(ref=@Model(type=Movie::class)) 110 | * ) 111 | * ) 112 | * 113 | * @param Request $request 114 | * @param Movie|null $movie 115 | * 116 | * @return JsonResponse 117 | * 118 | * @Security("is_granted('CAN_CREATE_MOVIE', movie)") 119 | */ 120 | public function createAction(Request $request, Movie $movie = null): JsonResponse 121 | { 122 | if (false === !!$movie) { 123 | $movie = new Movie(); 124 | } 125 | 126 | $form = $this->getForm( 127 | MovieType::class, 128 | $movie, 129 | [ 130 | 'method' => $request->getMethod(), 131 | ] 132 | ); 133 | 134 | try { 135 | $this->formHandler->process($request, $form); 136 | } catch (ApiException $e) { 137 | return new JsonResponse($e->getMessage(), Response::HTTP_BAD_REQUEST); 138 | } 139 | 140 | return $this->createResourceResponse($movie, Response::HTTP_CREATED); 141 | } 142 | 143 | /** 144 | * Edit existing Movie. 145 | * 146 | * @Route(path="/{movie}", name="api_movie_edit", methods={Request::METHOD_PATCH}) 147 | * 148 | * @SWG\Tag(name="Movie") 149 | * @SWG\Response( 150 | * response=200, 151 | * description="Updates Movie of given identifier and returns the updated object.", 152 | * @SWG\Schema( 153 | * type="object", 154 | * @SWG\Items(ref=@Model(type=Movie::class)) 155 | * ) 156 | * ) 157 | * 158 | * @param Request $request 159 | * @param Movie|null $movie 160 | * 161 | * @return JsonResponse 162 | * 163 | * @Security("is_granted('CAN_UPDATE_MOVIE', movie)") 164 | */ 165 | public function updateAction(Request $request, Movie $movie = null): JsonResponse 166 | { 167 | if (false === !!$movie) { 168 | return $this->createNotFoundResponse(); 169 | } 170 | 171 | $form = $this->getForm( 172 | MovieType::class, 173 | $movie, 174 | [ 175 | 'method' => $request->getMethod(), 176 | ] 177 | ); 178 | 179 | try { 180 | $this->formHandler->process($request, $form); 181 | } catch (ApiException $e) { 182 | return new JsonResponse($e->getMessage(), Response::HTTP_BAD_REQUEST); 183 | } 184 | 185 | return $this->createResourceResponse($movie); 186 | } 187 | 188 | /** 189 | * Delete Movie. 190 | * 191 | * @Route(path="/{movie}", name="api_movie_delete", methods={Request::METHOD_DELETE}) 192 | * 193 | * @SWG\Tag(name="Movie") 194 | * @SWG\Response( 195 | * response=200, 196 | * description="Delete Movie of given identifier and returns the empty object.", 197 | * @SWG\Schema( 198 | * type="object", 199 | * @SWG\Items(ref=@Model(type=Movie::class)) 200 | * ) 201 | * ) 202 | * 203 | * @param Movie|null $movie 204 | * 205 | * @return JsonResponse 206 | * 207 | * @Security("is_granted('CAN_DELETE_MOVIE', movie)") 208 | */ 209 | public function deleteAction(Movie $movie = null): JsonResponse 210 | { 211 | if (false === !!$movie) { 212 | return $this->createNotFoundResponse(); 213 | } 214 | 215 | try { 216 | $this->entityManager->remove($movie); 217 | $this->entityManager->flush(); 218 | } catch (\Exception $exception) { 219 | return $this->createGenericErrorResponse($exception); 220 | } 221 | 222 | return $this->createSuccessfulApiResponse(self::DELETED); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/Controller/README.md: -------------------------------------------------------------------------------- 1 | # Controllers 2 | 3 | ### Abstract controller 4 | 5 | Creating `$pagitor` with `DoctrineORMAdapter` is probably the most common use case. 6 | 7 | You can also implement `createMongoPaginagor(Request $request, Query $query)` using `DoctrineORMAdapter` in a very similar way. 8 | 9 | If you need method `elasticSearchPagination()` you can build according to [Pagerfanta Documentation]((https://github.com/whiteoctober/Pagerfanta#elasticaadapter)) 10 | 11 | ```php 12 | setMaxPerPage($request->query->get('limit', 10)); 23 | $paginator->setCurrentPage($request->query->get('page', 1)); 24 | 25 | return $paginator; 26 | } 27 | } 28 | ``` 29 | 30 | This method allows you to forget about handling `LexikFormFilter` and simplify listing your data with filters controllers. 31 | 32 | ```php 33 | getRepository(); 40 | $queryBuilder = $repository->getQueryBuilder(); 41 | 42 | $form = $this->getForm($filterForm); 43 | 44 | if ($request->query->has($form->getName())) { 45 | $form->submit($request->query->get($form->getName())); 46 | 47 | $queryBuilder = $this->get('lexik_form_filter.query_builder_updater') 48 | ->addFilterConditions($form, $queryBuilder); 49 | } 50 | 51 | $paginagor = $this->createPaginator($request, $queryBuilder->getQuery()); 52 | 53 | return $paginagor; 54 | } 55 | } 56 | ``` 57 | 58 | # Controllers 59 | 60 | Controllers follow implements `list`, `show`, `create`, `update` and `delete` actions. 61 | 62 | I implemented **GET**, **POST**, **PATCH** and **DELETE** request methods. I haven't included **PUT** in favor of **PATCH**. 63 | 64 | It's up to you what methods do you want to implement and what additional endpoint you need to create. 65 | -------------------------------------------------------------------------------- /src/Controller/ReviewController.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Controller; 13 | 14 | use App\Entity\Review; 15 | use App\Exception\ApiException; 16 | use App\Form\Filter\ReviewFilter; 17 | use App\Form\ReviewType; 18 | use App\Interfaces\ControllerInterface; 19 | use Nelmio\ApiDocBundle\Annotation\Model; 20 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; 21 | use Swagger\Annotations as SWG; 22 | use Symfony\Component\HttpFoundation\JsonResponse; 23 | use Symfony\Component\HttpFoundation\Request; 24 | use Symfony\Component\HttpFoundation\Response; 25 | use Symfony\Component\Routing\Annotation\Route; 26 | 27 | /** 28 | * @Route(path="/reviews") 29 | */ 30 | class ReviewController extends AbstractController implements ControllerInterface 31 | { 32 | /** 33 | * ReviewController constructor. 34 | */ 35 | public function __construct() 36 | { 37 | parent::__construct(Review::class); 38 | } 39 | 40 | /** 41 | * Get all Reviews. 42 | * 43 | * @Route(name="api_review_list", methods={Request::METHOD_GET}) 44 | * 45 | * @SWG\Tag(name="Review") 46 | * @SWG\Response( 47 | * response=200, 48 | * description="Returns the list of reviews", 49 | * @SWG\Schema( 50 | * type="array", 51 | * @SWG\Items(ref=@Model(type=Review::class)) 52 | * ) 53 | * ) 54 | * 55 | * @param Request $request 56 | * 57 | * @return JsonResponse 58 | */ 59 | public function listAction(Request $request): JsonResponse 60 | { 61 | return $this->createCollectionResponse( 62 | $this->handleFilterForm( 63 | $request, 64 | ReviewFilter::class 65 | ) 66 | ); 67 | } 68 | 69 | /** 70 | * Show single Reviews. 71 | * 72 | * @Route(path="/{review}", name="api_review_show", methods={Request::METHOD_GET}) 73 | * 74 | * @SWG\Tag(name="Review") 75 | * @SWG\Response( 76 | * response=200, 77 | * description="Returns review of given identifier.", 78 | * @SWG\Schema( 79 | * type="object", 80 | * title="review", 81 | * @SWG\Items(ref=@Model(type=Review::class)) 82 | * ) 83 | * ) 84 | * 85 | * @param Review|null $review 86 | * 87 | * @return JsonResponse 88 | */ 89 | public function showAction(Review $review = null): JsonResponse 90 | { 91 | if (false === !!$review) { 92 | return $this->createNotFoundResponse(); 93 | } 94 | 95 | return $this->createResourceResponse($review); 96 | } 97 | 98 | /** 99 | * Add new Review. 100 | * 101 | * @Route(name="api_review_create", methods={Request::METHOD_POST}) 102 | * 103 | * @SWG\Tag(name="Review") 104 | * @SWG\Response( 105 | * response=200, 106 | * description="Updates Review of given identifier and returns the updated object.", 107 | * @SWG\Schema( 108 | * type="object", 109 | * @SWG\Items(ref=@Model(type=Review::class)) 110 | * ) 111 | * ) 112 | * 113 | * @param Request $request 114 | * @param Review $review 115 | * 116 | * @return JsonResponse 117 | * 118 | * @Security("is_granted('CAN_CREATE_REVIEW', review)") 119 | */ 120 | public function createAction(Request $request, Review $review = null): JsonResponse 121 | { 122 | if (false === !!$review) { 123 | $review = new Review(); 124 | $review->setAuthor($this->getUser()); 125 | } 126 | 127 | $form = $this->getForm( 128 | ReviewType::class, 129 | $review, 130 | [ 131 | 'method' => $request->getMethod(), 132 | ] 133 | ); 134 | 135 | try { 136 | $this->formHandler->process($request, $form); 137 | } catch (ApiException $e) { 138 | return new JsonResponse($e->getData(), Response::HTTP_BAD_REQUEST); 139 | } 140 | 141 | return $this->createResourceResponse($review, Response::HTTP_CREATED); 142 | } 143 | 144 | /** 145 | * Edit existing Review. 146 | * 147 | * @Route(path="/{review}", name="api_review_edit", methods={Request::METHOD_PATCH}) 148 | * 149 | * @SWG\Tag(name="Review") 150 | * @SWG\Response( 151 | * response=200, 152 | * description="Updates Review of given identifier and returns the updated object.", 153 | * @SWG\Schema( 154 | * type="object", 155 | * @SWG\Items(ref=@Model(type=Review::class)) 156 | * ) 157 | * ) 158 | * 159 | * @param Request $request 160 | * @param Review|null $review 161 | * 162 | * @return JsonResponse 163 | * 164 | * @Security("is_granted('CAN_UPDATE_REVIEW', review)") 165 | */ 166 | public function updateAction(Request $request, Review $review = null): JsonResponse 167 | { 168 | if (false === !!$review) { 169 | return $this->createNotFoundResponse(); 170 | } 171 | 172 | $form = $this->getForm( 173 | ReviewType::class, 174 | $review, 175 | [ 176 | 'method' => $request->getMethod(), 177 | ] 178 | ); 179 | 180 | try { 181 | $this->formHandler->process($request, $form); 182 | } catch (ApiException $e) { 183 | return new JsonResponse($e->getMessage(), Response::HTTP_BAD_REQUEST); 184 | } 185 | 186 | return $this->createResourceResponse($review); 187 | } 188 | 189 | /** 190 | * Delete Review. 191 | * 192 | * @Route(path="/{review}", name="api_review_delete", methods={Request::METHOD_DELETE}) 193 | * 194 | * @SWG\Tag(name="Review") 195 | * @SWG\Response( 196 | * response=200, 197 | * description="Delete Review of given identifier and returns the empty object.", 198 | * @SWG\Schema( 199 | * type="object", 200 | * @SWG\Items(ref=@Model(type=Review::class)) 201 | * ) 202 | * ) 203 | * 204 | * @param Review|null $review 205 | * 206 | * @return JsonResponse 207 | * 208 | * @Security("is_granted('CAN_DELETE_REVIEW', review)") 209 | */ 210 | public function deleteAction(Review $review = null): JsonResponse 211 | { 212 | if (false === !!$review) { 213 | return $this->createNotFoundResponse(); 214 | } 215 | 216 | try { 217 | $this->entityManager->remove($review); 218 | $this->entityManager->flush(); 219 | } catch (\Exception $exception) { 220 | return $this->createGenericErrorResponse($exception); 221 | } 222 | 223 | return $this->createSuccessfulApiResponse(self::DELETED); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/Controller/UserController.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Controller; 13 | 14 | use App\Entity\User; 15 | use App\Exception\ApiException; 16 | use App\Form\Filter\UserFilter; 17 | use App\Form\UserType; 18 | use App\Interfaces\ControllerInterface; 19 | use Nelmio\ApiDocBundle\Annotation\Model; 20 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; 21 | use Swagger\Annotations as SWG; 22 | use Symfony\Component\HttpFoundation\JsonResponse; 23 | use Symfony\Component\HttpFoundation\Request; 24 | use Symfony\Component\HttpFoundation\Response; 25 | use Symfony\Component\Routing\Annotation\Route; 26 | 27 | /** 28 | * @Route(path="/users") 29 | */ 30 | class UserController extends AbstractController implements ControllerInterface 31 | { 32 | /** 33 | * UserController constructor. 34 | */ 35 | public function __construct() 36 | { 37 | parent::__construct(User::class); 38 | } 39 | 40 | /** 41 | * Get all Users. 42 | * 43 | * @Route(name="api_user_list", methods={Request::METHOD_GET}) 44 | * 45 | * @SWG\Tag(name="User") 46 | * @SWG\Response( 47 | * response=200, 48 | * description="Returns the list of users", 49 | * @SWG\Schema( 50 | * type="array", 51 | * @SWG\Items(ref=@Model(type=User::class)) 52 | * ) 53 | * ) 54 | * 55 | * @param Request $request 56 | * 57 | * @return JsonResponse 58 | */ 59 | public function listAction(Request $request): JsonResponse 60 | { 61 | return $this->createCollectionResponse( 62 | $this->handleFilterForm( 63 | $request, 64 | UserFilter::class 65 | ) 66 | ); 67 | } 68 | 69 | /** 70 | * Show single Users. 71 | * 72 | * @Route(path="/{user}", name="api_user_show", methods={Request::METHOD_GET}) 73 | * 74 | * @SWG\Tag(name="User") 75 | * @SWG\Response( 76 | * response=200, 77 | * description="Returns user of given identifier.", 78 | * @SWG\Schema( 79 | * type="object", 80 | * title="user", 81 | * @SWG\Items(ref=@Model(type=User::class)) 82 | * ) 83 | * ) 84 | * 85 | * @param User|null $user 86 | * 87 | * @return JsonResponse 88 | */ 89 | public function showAction(User $user = null): JsonResponse 90 | { 91 | if (false === !!$user) { 92 | return $this->createNotFoundResponse(); 93 | } 94 | 95 | return $this->createResourceResponse($user); 96 | } 97 | 98 | /** 99 | * Add new User. 100 | * 101 | * @Route(name="api_user_create", methods={Request::METHOD_POST}) 102 | * 103 | * @SWG\Tag(name="User") 104 | * @SWG\Response( 105 | * response=200, 106 | * description="Updates User of given identifier and returns the updated object.", 107 | * @SWG\Schema( 108 | * type="object", 109 | * @SWG\Items(ref=@Model(type=User::class)) 110 | * ) 111 | * ) 112 | * 113 | * @param Request $request 114 | * @param User|null $user 115 | * 116 | * @return JsonResponse 117 | */ 118 | public function createAction(Request $request, User $user = null): JsonResponse 119 | { 120 | if (false === !!$user) { 121 | $user = new User(); 122 | } 123 | 124 | $form = $this->getForm( 125 | UserType::class, 126 | $user, 127 | [ 128 | 'method' => $request->getMethod(), 129 | ] 130 | ); 131 | 132 | try { 133 | $this->formHandler->process($request, $form); 134 | } catch (ApiException $e) { 135 | return new JsonResponse($e->getData(), Response::HTTP_BAD_REQUEST); 136 | } 137 | 138 | return $this->createResourceResponse($user, Response::HTTP_CREATED); 139 | } 140 | 141 | /** 142 | * Edit existing User. 143 | * 144 | * @Route(path="/{user}", name="api_user_edit", methods={Request::METHOD_PATCH}) 145 | * 146 | * @SWG\Tag(name="User") 147 | * @SWG\Response( 148 | * response=200, 149 | * description="Updates User of given identifier and returns the updated object.", 150 | * @SWG\Schema( 151 | * type="object", 152 | * @SWG\Items(ref=@Model(type=User::class)) 153 | * ) 154 | * )* 155 | * 156 | * @param Request $request 157 | * @param User|null $user 158 | * 159 | * @return JsonResponse 160 | * 161 | * @Security("is_granted('CAN_UPDATE_USER', user)") 162 | */ 163 | public function updateAction(Request $request, User $user = null): JsonResponse 164 | { 165 | if (false === !!$user) { 166 | return $this->createNotFoundResponse(); 167 | } 168 | 169 | $form = $this->getForm( 170 | UserType::class, 171 | $user, 172 | [ 173 | 'method' => $request->getMethod(), 174 | ] 175 | ); 176 | 177 | try { 178 | $this->formHandler->process($request, $form); 179 | } catch (ApiException $e) { 180 | return new JsonResponse($e->getMessage(), Response::HTTP_BAD_REQUEST); 181 | } 182 | 183 | return $this->createResourceResponse($user); 184 | } 185 | 186 | /** 187 | * Delete User. 188 | * 189 | * @Route(path="/{user}", name="api_user_delete", methods={Request::METHOD_DELETE}) 190 | * 191 | * @SWG\Tag(name="User") 192 | * @SWG\Response( 193 | * response=200, 194 | * description="Delete User of given identifier and returns the empty object.", 195 | * @SWG\Schema( 196 | * type="object", 197 | * @SWG\Items(ref=@Model(type=User::class)) 198 | * ) 199 | * ) 200 | * 201 | * @param User $user 202 | * 203 | * @return JsonResponse 204 | * 205 | * @Security("is_granted('CAN_DELETE_USER', user)") 206 | */ 207 | public function deleteAction(User $user = null): JsonResponse 208 | { 209 | if (false === !!$user) { 210 | return $this->createNotFoundResponse(); 211 | } 212 | 213 | try { 214 | $this->entityManager->remove($user); 215 | $this->entityManager->flush(); 216 | } catch (\Exception $exception) { 217 | return $this->createGenericErrorResponse($exception); 218 | } 219 | 220 | return $this->createSuccessfulApiResponse(self::DELETED); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/Entity/AbstractUser.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Entity; 13 | 14 | use App\EventSubscriber\UserSubscriber; 15 | use App\Traits\IdColumnTrait; 16 | use App\Traits\TimeAwareTrait; 17 | use Doctrine\ORM\Mapping as ORM; 18 | use JMS\Serializer\Annotation as JMS; 19 | use Symfony\Component\Security\Core\User\UserInterface; 20 | use Symfony\Component\Validator\Constraints as Assert; 21 | 22 | /** 23 | * @JMS\ExclusionPolicy("ALL") 24 | */ 25 | abstract class AbstractUser implements UserInterface, \Serializable 26 | { 27 | use IdColumnTrait; 28 | use TimeAwareTrait; 29 | 30 | /** 31 | * @var string 32 | * 33 | * @ORM\Column(type="string") 34 | * @Assert\NotBlank 35 | * 36 | * @JMS\Expose 37 | */ 38 | protected $fullName; 39 | 40 | /** 41 | * @var string 42 | * 43 | * @see UserSubscriber::onFlush 44 | * 45 | * @ORM\Column(type="string", unique=true) 46 | */ 47 | protected $username; 48 | 49 | /** 50 | * @var string 51 | * 52 | * @ORM\Column(type="string", unique=true) 53 | * 54 | * @Assert\Email 55 | * @Assert\NotBlank 56 | * 57 | * @JMS\Expose 58 | */ 59 | protected $email; 60 | 61 | /** 62 | * @var string 63 | */ 64 | protected $plainPassword; 65 | 66 | /** 67 | * @var string 68 | * 69 | * @ORM\Column(type="string") 70 | */ 71 | protected $password; 72 | 73 | /** 74 | * @var array 75 | * 76 | * @ORM\Column(type="json") 77 | */ 78 | protected $roles = []; 79 | 80 | /** 81 | * @return string 82 | */ 83 | public function __toString(): string 84 | { 85 | return $this->username; 86 | } 87 | 88 | /** 89 | * @return int 90 | */ 91 | public function getId(): int 92 | { 93 | return $this->id; 94 | } 95 | 96 | /** 97 | * @param string $fullName 98 | */ 99 | public function setFullName(string $fullName): void 100 | { 101 | $this->fullName = $fullName; 102 | } 103 | 104 | /** 105 | * @return string 106 | */ 107 | public function getFullName(): ?string 108 | { 109 | return $this->fullName; 110 | } 111 | 112 | /** 113 | * @return string 114 | */ 115 | public function getUsername(): ?string 116 | { 117 | return $this->email; 118 | } 119 | 120 | /** 121 | * @param string $username 122 | */ 123 | public function setUsername(string $username): void 124 | { 125 | $this->username = $username; 126 | } 127 | 128 | /** 129 | * @return string 130 | */ 131 | public function getEmail(): ?string 132 | { 133 | return $this->email; 134 | } 135 | 136 | /** 137 | * @param string $email 138 | */ 139 | public function setEmail(string $email): void 140 | { 141 | $this->email = $email; 142 | } 143 | 144 | /** 145 | * @param string $plainPassword 146 | */ 147 | public function setPlainPassword(string $plainPassword): void 148 | { 149 | $this->plainPassword = $plainPassword; 150 | } 151 | 152 | /** 153 | * @return string|null 154 | */ 155 | public function getPlainPassword(): ?string 156 | { 157 | return $this->plainPassword; 158 | } 159 | 160 | /** 161 | * @return string|null 162 | */ 163 | public function getPassword(): ?string 164 | { 165 | return $this->password; 166 | } 167 | 168 | public function setPassword(string $password): void 169 | { 170 | $this->password = $password; 171 | } 172 | 173 | /** 174 | * Returns the roles or permissions granted to the user for security. 175 | */ 176 | public function getRoles(): array 177 | { 178 | $roles = $this->roles; 179 | 180 | // guarantees that a user always has at least one role for security 181 | if (empty($roles)) { 182 | $roles[] = 'ROLE_USER'; 183 | } 184 | 185 | return array_unique($roles); 186 | } 187 | 188 | public function setRoles(array $roles): void 189 | { 190 | $this->roles = $roles; 191 | } 192 | 193 | /** 194 | * Returns the salt that was originally used to encode the password. 195 | * 196 | * {@inheritdoc} 197 | */ 198 | public function getSalt(): ?string 199 | { 200 | // See "Do you need to use a Salt?" at https://symfony.com/doc/current/cookbook/security/entity_provider.html 201 | // we're using bcrypt in security.yml to encode the password, so 202 | // the salt value is built-in and you don't have to generate one 203 | 204 | return null; 205 | } 206 | 207 | /** 208 | * Removes sensitive data from the user. 209 | * 210 | * {@inheritdoc} 211 | */ 212 | public function eraseCredentials(): void 213 | { 214 | $this->plainPassword = null; 215 | } 216 | 217 | /** 218 | * {@inheritdoc} 219 | */ 220 | public function serialize(): ?string 221 | { 222 | return serialize([$this->id, $this->username, $this->password]); 223 | } 224 | 225 | /** 226 | * {@inheritdoc} 227 | */ 228 | public function unserialize($serialized): void 229 | { 230 | [$this->id, $this->username, $this->password] = unserialize($serialized, ['allowed_classes' => false]); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/Entity/Book.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Entity; 13 | 14 | use App\Traits\IdColumnTrait; 15 | use App\Traits\TimeAwareTrait; 16 | use Doctrine\Common\Collections\ArrayCollection; 17 | use Doctrine\Common\Collections\Collection; 18 | use Doctrine\ORM\Mapping as ORM; 19 | use JMS\Serializer\Annotation as JMS; 20 | use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; 21 | use Symfony\Component\Validator\Constraints as Assert; 22 | 23 | /** 24 | * @ORM\Entity(repositoryClass="App\Repository\BookRepository") 25 | * 26 | * @UniqueEntity({"isbn"}, message="Book with this ISBN already exists,") 27 | * 28 | * @JMS\ExclusionPolicy("ALL") 29 | */ 30 | class Book 31 | { 32 | use IdColumnTrait; 33 | use TimeAwareTrait; 34 | 35 | /** 36 | * @var string 37 | * 38 | * @ORM\Column(type="string") 39 | * 40 | * @Assert\NotBlank 41 | * 42 | * @JMS\Expose 43 | */ 44 | protected $isbn; 45 | 46 | /** 47 | * @var string 48 | * 49 | * @ORM\Column(type="string") 50 | * 51 | * @Assert\NotBlank 52 | * 53 | * @JMS\Expose 54 | */ 55 | protected $title; 56 | 57 | /** 58 | * @var string 59 | * 60 | * @ORM\Column(type="text") 61 | * 62 | * @Assert\NotBlank 63 | * 64 | * @JMS\Expose 65 | */ 66 | protected $description; 67 | 68 | /** 69 | * @var string 70 | * 71 | * @ORM\Column(type="text") 72 | * 73 | * @Assert\NotBlank 74 | * 75 | * @JMS\Expose 76 | */ 77 | protected $author; 78 | 79 | /** 80 | * @var \DateTimeInterface 81 | * 82 | * @ORM\Column(type="datetime") 83 | * 84 | * @Assert\NotBlank 85 | * 86 | * @JMS\Expose 87 | */ 88 | protected $publicationDate; 89 | 90 | /** 91 | * @var ArrayCollection|Review[] 92 | * 93 | * @ORM\OneToMany(targetEntity="App\Entity\Review", mappedBy="book", cascade={"all"}) 94 | * 95 | * @JMS\Expose 96 | * @JMS\Groups("reviews") 97 | */ 98 | protected $reviews; 99 | 100 | /** 101 | * @var ArrayCollection|User[] 102 | * 103 | * @ORM\ManyToMany(targetEntity="App\Entity\User", mappedBy="books", cascade={"all"}) 104 | * 105 | * @JMS\Expose 106 | * @JMS\Groups("readers") 107 | */ 108 | protected $readers; 109 | 110 | /** 111 | * Book constructor. 112 | */ 113 | public function __construct() 114 | { 115 | $this->reviews = new ArrayCollection(); 116 | $this->readers = new ArrayCollection(); 117 | } 118 | 119 | /** 120 | * @return string|null 121 | */ 122 | public function getIsbn(): ?string 123 | { 124 | return $this->isbn; 125 | } 126 | 127 | /** 128 | * @param string $isbn 129 | * 130 | * @return Book 131 | */ 132 | public function setIsbn(string $isbn): self 133 | { 134 | $this->isbn = $isbn; 135 | 136 | return $this; 137 | } 138 | 139 | /** 140 | * @return string|null 141 | */ 142 | public function getTitle(): ?string 143 | { 144 | return $this->title; 145 | } 146 | 147 | /** 148 | * @param string $title 149 | * 150 | * @return Book 151 | */ 152 | public function setTitle(string $title): self 153 | { 154 | $this->title = $title; 155 | 156 | return $this; 157 | } 158 | 159 | /** 160 | * @return string|null 161 | */ 162 | public function getDescription(): ?string 163 | { 164 | return $this->description; 165 | } 166 | 167 | /** 168 | * @param string $description 169 | * 170 | * @return Book 171 | */ 172 | public function setDescription(string $description): self 173 | { 174 | $this->description = $description; 175 | 176 | return $this; 177 | } 178 | 179 | /** 180 | * @return string|null 181 | */ 182 | public function getAuthor(): ?string 183 | { 184 | return $this->author; 185 | } 186 | 187 | /** 188 | * @param string $author 189 | * 190 | * @return Book 191 | */ 192 | public function setAuthor(string $author): self 193 | { 194 | $this->author = $author; 195 | 196 | return $this; 197 | } 198 | 199 | /** 200 | * @return \DateTimeInterface|null 201 | */ 202 | public function getPublicationDate(): ?\DateTimeInterface 203 | { 204 | return $this->publicationDate; 205 | } 206 | 207 | /** 208 | * @param \DateTimeInterface $publicationDate 209 | * 210 | * @return Book 211 | */ 212 | public function setPublicationDate(?\DateTimeInterface $publicationDate): self 213 | { 214 | $this->publicationDate = $publicationDate; 215 | 216 | return $this; 217 | } 218 | 219 | /** 220 | * @return Collection|Review[] 221 | */ 222 | public function getReviews(): Collection 223 | { 224 | return $this->reviews; 225 | } 226 | 227 | /** 228 | * @param Review $review 229 | * 230 | * @return Book 231 | */ 232 | public function addReview(Review $review): self 233 | { 234 | if (!$this->reviews->contains($review)) { 235 | $this->reviews[] = $review; 236 | $review->setBook($this); 237 | } 238 | 239 | return $this; 240 | } 241 | 242 | /** 243 | * @param Review $review 244 | * 245 | * @return Book 246 | */ 247 | public function removeReview(Review $review): self 248 | { 249 | if ($this->reviews->contains($review)) { 250 | $this->reviews->removeElement($review); 251 | // set the owning side to null (unless already changed) 252 | if ($review->getBook() === $this) { 253 | $review->setBook(null); 254 | } 255 | } 256 | 257 | return $this; 258 | } 259 | 260 | /** 261 | * @return Collection 262 | */ 263 | public function getReaders(): Collection 264 | { 265 | return $this->readers; 266 | } 267 | 268 | /** 269 | * @param User $reader 270 | * 271 | * @return Book 272 | */ 273 | public function addReader(User $reader): self 274 | { 275 | if (!$this->readers->contains($reader)) { 276 | $this->readers[] = $reader; 277 | $reader->addBook($this); 278 | } 279 | 280 | return $this; 281 | } 282 | 283 | /** 284 | * @param User $reader 285 | * 286 | * @return Book 287 | */ 288 | public function removeReader(User $reader): self 289 | { 290 | if ($this->readers->contains($reader)) { 291 | $this->readers->removeElement($reader); 292 | $reader->removeBook($this); 293 | } 294 | 295 | return $this; 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /src/Entity/Movie.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Entity; 13 | 14 | use App\Traits\IdColumnTrait; 15 | use App\Traits\TimeAwareTrait; 16 | use Doctrine\Common\Collections\ArrayCollection; 17 | use Doctrine\Common\Collections\Collection; 18 | use Doctrine\ORM\Mapping as ORM; 19 | use JMS\Serializer\Annotation as JMS; 20 | use Symfony\Component\Validator\Constraints as Assert; 21 | 22 | /** 23 | * @ORM\Entity(repositoryClass="App\Repository\MovieRepository") 24 | * 25 | * @JMS\ExclusionPolicy("ALL") 26 | */ 27 | class Movie 28 | { 29 | use IdColumnTrait; 30 | use TimeAwareTrait; 31 | 32 | /** 33 | * @var int 34 | * 35 | * @ORM\Column(type="integer") 36 | * 37 | * @Assert\NotBlank 38 | * 39 | * @JMS\Expose 40 | */ 41 | protected $duration; 42 | 43 | /** 44 | * @var string 45 | * 46 | * @ORM\Column(type="string") 47 | * 48 | * @Assert\NotBlank 49 | * 50 | * @JMS\Expose 51 | */ 52 | protected $title; 53 | 54 | /** 55 | * @var string 56 | * 57 | * @ORM\Column(type="text") 58 | * 59 | * @Assert\NotBlank 60 | */ 61 | protected $description; 62 | 63 | /** 64 | * @var string 65 | * 66 | * @ORM\Column(type="text") 67 | * 68 | * @Assert\NotBlank 69 | * 70 | * @JMS\Expose 71 | */ 72 | protected $director; 73 | 74 | /** 75 | * @var \DateTimeInterface 76 | * 77 | * @ORM\Column(type="datetime") 78 | * 79 | * @Assert\NotBlank 80 | * 81 | * @JMS\Expose 82 | */ 83 | protected $publicationDate; 84 | 85 | /** 86 | * @var ArrayCollection|Review[] 87 | * 88 | * @ORM\OneToMany(targetEntity="App\Entity\Review", mappedBy="movie") 89 | * 90 | * @JMS\Expose 91 | * @JMS\Groups("reviews") 92 | */ 93 | protected $reviews; 94 | 95 | /** 96 | * @var ArrayCollection|User[] 97 | * 98 | * @ORM\ManyToMany(targetEntity="App\Entity\User", mappedBy="movies") 99 | * 100 | * @JMS\Expose 101 | * @JMS\Groups("audience") 102 | */ 103 | protected $audience; 104 | 105 | /** 106 | * Movie constructor. 107 | */ 108 | public function __construct() 109 | { 110 | $this->reviews = new ArrayCollection(); 111 | $this->audience = new ArrayCollection(); 112 | } 113 | 114 | /** 115 | * @return int|null 116 | */ 117 | public function getDuration(): ?int 118 | { 119 | return $this->duration; 120 | } 121 | 122 | /** 123 | * @param int $duration 124 | * 125 | * @return Movie 126 | */ 127 | public function setDuration(int $duration): self 128 | { 129 | $this->duration = $duration; 130 | 131 | return $this; 132 | } 133 | 134 | /** 135 | * @return string|null 136 | */ 137 | public function getTitle(): ?string 138 | { 139 | return $this->title; 140 | } 141 | 142 | /** 143 | * @param string|null $title 144 | * 145 | * @return Movie 146 | */ 147 | public function setTitle(string $title): self 148 | { 149 | $this->title = $title; 150 | 151 | return $this; 152 | } 153 | 154 | /** 155 | * @return string|null 156 | */ 157 | public function getDescription(): ?string 158 | { 159 | return $this->description; 160 | } 161 | 162 | /** 163 | * @param string $description 164 | * 165 | * @return Movie 166 | */ 167 | public function setDescription(string $description): self 168 | { 169 | $this->description = $description; 170 | 171 | return $this; 172 | } 173 | 174 | /** 175 | * @return string|null 176 | */ 177 | public function getDirector(): ?string 178 | { 179 | return $this->director; 180 | } 181 | 182 | /** 183 | * @param string $director 184 | * 185 | * @return Movie 186 | */ 187 | public function setDirector(string $director): self 188 | { 189 | $this->director = $director; 190 | 191 | return $this; 192 | } 193 | 194 | /** 195 | * @return \DateTimeInterface|null 196 | */ 197 | public function getPublicationDate(): ?\DateTimeInterface 198 | { 199 | return $this->publicationDate; 200 | } 201 | 202 | /** 203 | * @param \DateTimeInterface $publicationDate 204 | * 205 | * @return Movie 206 | */ 207 | public function setPublicationDate(?\DateTimeInterface $publicationDate): self 208 | { 209 | $this->publicationDate = $publicationDate; 210 | 211 | return $this; 212 | } 213 | 214 | /** 215 | * @return Collection|Review[] 216 | */ 217 | public function getReviews(): Collection 218 | { 219 | return $this->reviews; 220 | } 221 | 222 | /** 223 | * @param Review $review 224 | * 225 | * @return Movie 226 | */ 227 | public function addReview(Review $review): self 228 | { 229 | if (!$this->reviews->contains($review)) { 230 | $this->reviews[] = $review; 231 | $review->setMovie($this); 232 | } 233 | 234 | return $this; 235 | } 236 | 237 | /** 238 | * @param Review $review 239 | * 240 | * @return Movie 241 | */ 242 | public function removeReview(Review $review): self 243 | { 244 | if ($this->reviews->contains($review)) { 245 | $this->reviews->removeElement($review); 246 | // set the owning side to null (unless already changed) 247 | if ($review->getMovie() === $this) { 248 | $review->setMovie(null); 249 | } 250 | } 251 | 252 | return $this; 253 | } 254 | 255 | /** 256 | * @return Collection|User[] 257 | */ 258 | public function getAudience(): Collection 259 | { 260 | return $this->audience; 261 | } 262 | 263 | /** 264 | * @param User $audience 265 | * 266 | * @return Movie 267 | */ 268 | public function addAudience(User $audience): self 269 | { 270 | if (!$this->audience->contains($audience)) { 271 | $this->audience[] = $audience; 272 | $audience->addMovie($this); 273 | } 274 | 275 | return $this; 276 | } 277 | 278 | /** 279 | * @param User $audience 280 | * 281 | * @return Movie 282 | */ 283 | public function removeAudience(User $audience): self 284 | { 285 | if ($this->audience->contains($audience)) { 286 | $this->audience->removeElement($audience); 287 | $audience->removeMovie($this); 288 | } 289 | 290 | return $this; 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/Entity/README.md: -------------------------------------------------------------------------------- 1 | # Entities 2 | 3 | ## Exclusion Policy 4 | By default all entities uses `ExclusionPolicy("ALL")`. 5 | The property will not appear in response until you add `@JMS\Expose` in annotations. 6 | 7 | ```php 8 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Entity; 13 | 14 | use App\Traits\IdColumnTrait; 15 | use App\Traits\TimeAwareTrait; 16 | use Doctrine\ORM\Mapping as ORM; 17 | use JMS\Serializer\Annotation as JMS; 18 | use Symfony\Component\Validator\Constraints as Assert; 19 | 20 | /** 21 | * @ORM\Entity(repositoryClass="App\Repository\ReviewRepository") 22 | * 23 | * @JMS\ExclusionPolicy("ALL") 24 | */ 25 | class Review 26 | { 27 | use IdColumnTrait; 28 | use TimeAwareTrait; 29 | 30 | /** 31 | * @var string 32 | * 33 | * @ORM\Column(type="text") 34 | * 35 | * @Assert\NotBlank 36 | * 37 | * @JMS\Expose 38 | */ 39 | protected $body; 40 | 41 | /** 42 | * @var int 43 | * 44 | * @ORM\Column(type="integer") 45 | * 46 | * @Assert\NotBlank 47 | * 48 | * @JMS\Expose 49 | */ 50 | protected $rating; 51 | 52 | /** 53 | * @var User 54 | * 55 | * @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="reviews", cascade={"persist", "remove"}) 56 | * 57 | * @JMS\Expose 58 | * @JMS\Groups("author") 59 | */ 60 | protected $author; 61 | 62 | /** 63 | * @var \DateTimeInterface 64 | * 65 | * @ORM\Column(type="date") 66 | * 67 | * @Assert\NotBlank 68 | * 69 | * @JMS\Expose 70 | */ 71 | protected $publicationDate; 72 | 73 | /** 74 | * @var Book 75 | * 76 | * @ORM\ManyToOne(targetEntity="App\Entity\Book", inversedBy="reviews", cascade={"persist"}) 77 | * 78 | * @JMS\Expose 79 | * @JMS\Groups("books") 80 | */ 81 | protected $book; 82 | 83 | /** 84 | * @var Movie 85 | * 86 | * @ORM\ManyToOne(targetEntity="App\Entity\Movie", inversedBy="reviews", cascade={"persist"}) 87 | * 88 | * @JMS\Expose 89 | * @JMS\Groups("movies") 90 | */ 91 | protected $movie; 92 | 93 | /** 94 | * @return string|null 95 | */ 96 | public function getBody(): ?string 97 | { 98 | return $this->body; 99 | } 100 | 101 | /** 102 | * @param string $body 103 | * 104 | * @return Review 105 | */ 106 | public function setBody(string $body): self 107 | { 108 | $this->body = $body; 109 | 110 | return $this; 111 | } 112 | 113 | /** 114 | * @return int|null 115 | */ 116 | public function getRating(): ?int 117 | { 118 | return $this->rating; 119 | } 120 | 121 | /** 122 | * @param int $rating 123 | * 124 | * @return Review 125 | */ 126 | public function setRating(int $rating): self 127 | { 128 | $this->rating = $rating; 129 | 130 | return $this; 131 | } 132 | 133 | /** 134 | * @return \DateTimeInterface|null 135 | */ 136 | public function getPublicationDate(): ?\DateTimeInterface 137 | { 138 | return $this->publicationDate; 139 | } 140 | 141 | /** 142 | * @param \DateTimeInterface $publicationDate 143 | * 144 | * @return Review 145 | */ 146 | public function setPublicationDate(?\DateTimeInterface $publicationDate): self 147 | { 148 | $this->publicationDate = $publicationDate; 149 | 150 | return $this; 151 | } 152 | 153 | /** 154 | * @return User|null 155 | */ 156 | public function getAuthor(): ?User 157 | { 158 | return $this->author; 159 | } 160 | 161 | /** 162 | * @param User|null $author 163 | * 164 | * @return Review 165 | */ 166 | public function setAuthor(?User $author): self 167 | { 168 | $this->author = $author; 169 | 170 | return $this; 171 | } 172 | 173 | /** 174 | * @return Book|null 175 | */ 176 | public function getBook(): ?Book 177 | { 178 | return $this->book; 179 | } 180 | 181 | /** 182 | * @param Book|null $book 183 | * 184 | * @return Review 185 | */ 186 | public function setBook(?Book $book): self 187 | { 188 | $this->book = $book; 189 | 190 | return $this; 191 | } 192 | 193 | /** 194 | * @return Movie|null 195 | */ 196 | public function getMovie(): ?Movie 197 | { 198 | return $this->movie; 199 | } 200 | 201 | /** 202 | * @param Movie|null $movie 203 | * 204 | * @return Review 205 | */ 206 | public function setMovie(?Movie $movie): self 207 | { 208 | $this->movie = $movie; 209 | 210 | return $this; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/Entity/User.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Entity; 13 | 14 | use Doctrine\Common\Collections\ArrayCollection; 15 | use Doctrine\Common\Collections\Collection; 16 | use Doctrine\ORM\Mapping as ORM; 17 | use JMS\Serializer\Annotation as JMS; 18 | use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; 19 | use Symfony\Component\Security\Core\User\UserInterface; 20 | 21 | /** 22 | * @ORM\HasLifecycleCallbacks 23 | * @ORM\Entity(repositoryClass="App\Repository\UserRepository") 24 | * @ORM\Table(name="app_user") 25 | * 26 | * @UniqueEntity({"email"}, message="Email already exists.") 27 | * 28 | * @JMS\ExclusionPolicy("ALL") 29 | */ 30 | class User extends AbstractUser implements UserInterface 31 | { 32 | /** 33 | * @var ArrayCollection|Book[] 34 | * 35 | * @ORM\ManyToMany(targetEntity="App\Entity\Book", inversedBy="readers", cascade={"persist"}) 36 | * 37 | * @JMS\Expose 38 | * @JMS\Groups("books") 39 | */ 40 | protected $books; 41 | 42 | /** 43 | * @var ArrayCollection|Movie[] 44 | * 45 | * @ORM\ManyToMany(targetEntity="App\Entity\Movie", inversedBy="audience", cascade={"persist"}) 46 | * 47 | * @JMS\Expose 48 | * @JMS\Groups("movies") 49 | */ 50 | protected $movies; 51 | 52 | /** 53 | * @var ArrayCollection|Review[] 54 | * 55 | * @ORM\OneToMany(targetEntity="App\Entity\Review", mappedBy="author", cascade={"persist"}) 56 | * 57 | * @JMS\Expose 58 | * @JMS\Groups("reviews") 59 | */ 60 | protected $reviews; 61 | 62 | /** 63 | * User constructor. 64 | */ 65 | public function __construct() 66 | { 67 | $this->books = new ArrayCollection(); 68 | $this->movies = new ArrayCollection(); 69 | $this->reviews = new ArrayCollection(); 70 | } 71 | 72 | /** 73 | * @return Book[]|Collection 74 | */ 75 | public function getBooks(): Collection 76 | { 77 | return $this->books; 78 | } 79 | 80 | /** 81 | * @param Book $book 82 | * 83 | * @return User 84 | */ 85 | public function addBook(Book $book): self 86 | { 87 | if (!$this->books->contains($book)) { 88 | $this->books[] = $book; 89 | } 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * @param Book $book 96 | * 97 | * @return User 98 | */ 99 | public function removeBook(Book $book): self 100 | { 101 | if ($this->books->contains($book)) { 102 | $this->books->removeElement($book); 103 | } 104 | 105 | return $this; 106 | } 107 | 108 | /** 109 | * @return Collection|Movie[] 110 | */ 111 | public function getMovies(): Collection 112 | { 113 | return $this->movies; 114 | } 115 | 116 | /** 117 | * @param Movie $movie 118 | * 119 | * @return User 120 | */ 121 | public function addMovie(Movie $movie): self 122 | { 123 | if (!$this->movies->contains($movie)) { 124 | $this->movies[] = $movie; 125 | } 126 | 127 | return $this; 128 | } 129 | 130 | /** 131 | * @param Movie $movie 132 | * 133 | * @return User 134 | */ 135 | public function removeMovie(Movie $movie): self 136 | { 137 | if ($this->movies->contains($movie)) { 138 | $this->movies->removeElement($movie); 139 | } 140 | 141 | return $this; 142 | } 143 | 144 | /** 145 | * @return Collection|Review[] 146 | */ 147 | public function getReviews(): Collection 148 | { 149 | return $this->reviews; 150 | } 151 | 152 | /** 153 | * @param Review $review 154 | * 155 | * @return User 156 | */ 157 | public function addReview(Review $review): self 158 | { 159 | if (!$this->reviews->contains($review)) { 160 | $this->reviews[] = $review; 161 | $review->setAuthor($this); 162 | } 163 | 164 | return $this; 165 | } 166 | 167 | /** 168 | * @param Review $review 169 | * 170 | * @return User 171 | */ 172 | public function removeReview(Review $review): self 173 | { 174 | if ($this->reviews->contains($review)) { 175 | $this->reviews->removeElement($review); 176 | // set the owning side to null (unless already changed) 177 | if ($review->getAuthor() === $this) { 178 | $review->setAuthor(null); 179 | } 180 | } 181 | 182 | return $this; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/EventSubscriber/README.md: -------------------------------------------------------------------------------- 1 | # Event Subscriber 2 | 3 | `UserEventSubscriber` is responsible for encoding User `$plainPassword` to `$password` when user is _created_ or _updated_. 4 | -------------------------------------------------------------------------------- /src/EventSubscriber/UserSubscriber.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\EventSubscriber; 13 | 14 | use App\Entity\User; 15 | use Doctrine\Common\EventSubscriber; 16 | use Doctrine\ORM\Event\LifecycleEventArgs; 17 | use Doctrine\ORM\Event\OnFlushEventArgs; 18 | use Doctrine\ORM\Events; 19 | use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; 20 | 21 | class UserSubscriber implements EventSubscriber 22 | { 23 | /** 24 | * @var UserPasswordEncoderInterface 25 | */ 26 | protected $encoder; 27 | 28 | /** 29 | * UserSubscriber constructor. 30 | * 31 | * @param UserPasswordEncoderInterface $encoder 32 | */ 33 | public function __construct(UserPasswordEncoderInterface $encoder) 34 | { 35 | $this->encoder = $encoder; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function getSubscribedEvents(): array 42 | { 43 | return [ 44 | Events::prePersist, 45 | Events::postUpdate, 46 | Events::onFlush, 47 | ]; 48 | } 49 | 50 | /** 51 | * @param LifecycleEventArgs $args 52 | */ 53 | public function prePersist(LifecycleEventArgs $args): void 54 | { 55 | $user = $args->getEntity(); 56 | 57 | if ($user instanceof User) { 58 | $this->encodePassword($user); 59 | } 60 | } 61 | 62 | /** 63 | * @param LifecycleEventArgs $args 64 | */ 65 | public function postUpdate(LifecycleEventArgs $args): void 66 | { 67 | $user = $args->getEntity(); 68 | 69 | if ($user instanceof User) { 70 | $this->encodePassword($user); 71 | } 72 | } 73 | 74 | /** 75 | * @param OnFlushEventArgs $eventArgs 76 | */ 77 | public function onFlush(OnFlushEventArgs $eventArgs) 78 | { 79 | $em = $eventArgs->getEntityManager(); 80 | $uow = $em->getUnitOfWork(); 81 | $classMetadata = $em->getClassMetadata(User::class); 82 | 83 | foreach ($uow->getScheduledEntityInsertions() as $entity) { 84 | if ($entity instanceof User) { 85 | $entity->setUsername($entity->getEmail()); 86 | $uow->recomputeSingleEntityChangeSet($classMetadata, $entity); 87 | } 88 | } 89 | } 90 | 91 | /** 92 | * @param User $user 93 | */ 94 | protected function encodePassword(User $user): void 95 | { 96 | $encoded = $this->encoder->encodePassword($user, $user->getPlainPassword()); 97 | 98 | $user->setPassword($encoded); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Exception/ApiException.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Exception; 13 | 14 | use App\Exception\Enum\ApiErrorEnumType; 15 | use Exception; 16 | 17 | class ApiException extends Exception 18 | { 19 | public const DEFAULT_MESSAGE = 'General error.'; 20 | 21 | /** 22 | * @var string 23 | */ 24 | protected $type; 25 | 26 | /** 27 | * @var array 28 | */ 29 | protected $data; 30 | 31 | /** 32 | * {@inheritdoc} 33 | * 34 | * @param string $message 35 | * @param array $data 36 | */ 37 | public function __construct(string $message, int $code, array $data = []) 38 | { 39 | parent::__construct($message, $code); 40 | 41 | $this->type = ApiErrorEnumType::GENERAL_ERROR; 42 | $this->data = $data; 43 | } 44 | 45 | /** 46 | * @param array $data 47 | * 48 | * @return ApiException 49 | */ 50 | public static function createWithData(array $data = []): self 51 | { 52 | return new static(static::DEFAULT_MESSAGE, 0, $data); 53 | } 54 | 55 | /** 56 | * @return array 57 | */ 58 | public function getData(): array 59 | { 60 | return $this->data; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Exception/Enum/ApiErrorEnumType.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Exception\Enum; 13 | 14 | class ApiErrorEnumType 15 | { 16 | public const FORM_INVALID = 'form-invalid'; 17 | public const GENERAL_ERROR = 'general-error'; 18 | } 19 | -------------------------------------------------------------------------------- /src/Exception/FormInvalidException.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Exception; 13 | 14 | class FormInvalidException extends ApiException 15 | { 16 | public const DEFAULT_MESSAGE = 'Submitted form did not pass validation.'; 17 | 18 | /** 19 | * @param string $message 20 | * @param int $code 21 | * @param array $data 22 | */ 23 | public function __construct(string $message, int $code, array $data = []) 24 | { 25 | parent::__construct($message, $code, $data); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Form/BookType.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Form; 13 | 14 | use App\Entity\Book; 15 | use Symfony\Component\Form\AbstractType; 16 | use Symfony\Component\Form\Extension\Core\Type\DateTimeType; 17 | use Symfony\Component\Form\FormBuilderInterface; 18 | use Symfony\Component\OptionsResolver\OptionsResolver; 19 | 20 | class BookType extends AbstractType 21 | { 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function buildForm(FormBuilderInterface $builder, array $options) 26 | { 27 | $builder 28 | ->add('isbn') 29 | ->add('title') 30 | ->add('description') 31 | ->add('author') 32 | ->add('publicationDate', DateTimeType::class, [ 33 | 'widget' => 'single_text', 34 | 'input' => 'datetime', 35 | ]); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function configureOptions(OptionsResolver $resolver) 42 | { 43 | $resolver->setDefaults([ 44 | 'data_class' => Book::class, 45 | ]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Form/Filter/BookFilter.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Form\Filter; 13 | 14 | use Lexik\Bundle\FormFilterBundle\Filter\Form\Type as Filters; 15 | use Symfony\Component\Form\AbstractType; 16 | use Symfony\Component\Form\FormBuilderInterface; 17 | use Symfony\Component\OptionsResolver\OptionsResolver; 18 | 19 | class BookFilter extends AbstractType 20 | { 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function buildForm(FormBuilderInterface $builder, array $options) 25 | { 26 | $dateTimeFieldOptions = [ 27 | 'widget' => 'single_text', 28 | 'format' => 'yyyy-MM-dd', 29 | ]; 30 | 31 | $builder->add('isbn', Filters\TextFilterType::class) 32 | ->add('title', Filters\TextFilterType::class) 33 | ->add('description', Filters\TextFilterType::class) 34 | ->add('author', Filters\TextFilterType::class) 35 | ->add( 36 | 'publicationDate', 37 | Filters\DateTimeRangeFilterType::class, 38 | [ 39 | 'left_datetime_options' => $dateTimeFieldOptions, 40 | 'right_datetime_options' => $dateTimeFieldOptions, 41 | ] 42 | ); 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function configureOptions(OptionsResolver $resolver) 49 | { 50 | $resolver->setDefaults([ 51 | 'validation_groups' => ['filtering'], 52 | ]); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Form/Filter/MovieFilter.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Form\Filter; 13 | 14 | use Lexik\Bundle\FormFilterBundle\Filter\Form\Type as Filters; 15 | use Symfony\Component\Form\AbstractType; 16 | use Symfony\Component\Form\FormBuilderInterface; 17 | use Symfony\Component\OptionsResolver\OptionsResolver; 18 | 19 | class MovieFilter extends AbstractType 20 | { 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function buildForm(FormBuilderInterface $builder, array $options) 25 | { 26 | $dateTimeFieldOptions = [ 27 | 'widget' => 'single_text', 28 | 'format' => 'yyyy-MM-dd', 29 | ]; 30 | 31 | $builder 32 | ->add('duration', Filters\TextFilterType::class) 33 | ->add('title', Filters\TextFilterType::class) 34 | ->add('description', Filters\TextFilterType::class) 35 | ->add('author', Filters\TextFilterType::class) 36 | ->add( 37 | 'publicationDate', 38 | Filters\DateTimeRangeFilterType::class, 39 | [ 40 | 'left_datetime_options' => $dateTimeFieldOptions, 41 | 'right_datetime_options' => $dateTimeFieldOptions, 42 | ] 43 | ); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function configureOptions(OptionsResolver $resolver) 50 | { 51 | $resolver->setDefaults([ 52 | 'validation_groups' => ['filtering'], 53 | ]); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Form/Filter/ReviewFilter.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Form\Filter; 13 | 14 | use Lexik\Bundle\FormFilterBundle\Filter\Form\Type as Filters; 15 | use Symfony\Component\Form\AbstractType; 16 | use Symfony\Component\Form\FormBuilderInterface; 17 | use Symfony\Component\OptionsResolver\OptionsResolver; 18 | 19 | class ReviewFilter extends AbstractType 20 | { 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function buildForm(FormBuilderInterface $builder, array $options) 25 | { 26 | $dateTimeFieldOptions = [ 27 | 'widget' => 'single_text', 28 | 'format' => 'yyyy-MM-dd', 29 | ]; 30 | 31 | $builder 32 | ->add('body', Filters\TextFilterType::class) 33 | ->add('rating', Filters\TextFilterType::class) 34 | ->add( 35 | 'publicationDate', 36 | Filters\DateTimeRangeFilterType::class, 37 | [ 38 | 'left_datetime_options' => $dateTimeFieldOptions, 39 | 'right_datetime_options' => $dateTimeFieldOptions, 40 | ] 41 | ); 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function configureOptions(OptionsResolver $resolver) 48 | { 49 | $resolver->setDefaults([ 50 | 'validation_groups' => ['filtering'], 51 | ]); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Form/Filter/UserFilter.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Form\Filter; 13 | 14 | use Lexik\Bundle\FormFilterBundle\Filter\Form\Type as Filters; 15 | use Lexik\Bundle\FormFilterBundle\Filter\Query\QueryInterface; 16 | use Symfony\Component\Form\AbstractType; 17 | use Symfony\Component\Form\FormBuilderInterface; 18 | use Symfony\Component\OptionsResolver\OptionsResolver; 19 | 20 | class UserFilter extends AbstractType 21 | { 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function buildForm(FormBuilderInterface $builder, array $options) 26 | { 27 | // Allows filter Users movie title they watched. 28 | $builder 29 | ->add('email', Filters\TextFilterType::class) 30 | ->add('movies', Filters\TextFilterType::class, [ 31 | 'apply_filter' => function (QueryInterface $filterQuery, $field, $values) { 32 | if (empty($values['value'])) { 33 | return null; 34 | } 35 | 36 | $query = $filterQuery->getQueryBuilder(); 37 | $query->innerJoin($field, 't'); 38 | 39 | $paramName = sprintf('p_%s', str_replace('.', '_', $field)); 40 | $expression = $filterQuery->getExpr()->eq('t.title', ':'.$paramName); 41 | $parameters = [$paramName => $values['value']]; 42 | 43 | return $filterQuery->createCondition($expression, $parameters); 44 | }, 45 | ]); 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function configureOptions(OptionsResolver $resolver) 52 | { 53 | $resolver->setDefaults([ 54 | 'validation_groups' => ['filtering'], 55 | ]); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Form/Handler/AbstractFormHandler.php: -------------------------------------------------------------------------------- 1 | responseCreator = $responseCreator; 45 | $this->formErrorsSerializer = $formErrorsSerializer; 46 | $this->entityManager = $entityManager; 47 | } 48 | 49 | /** 50 | * @param Request $request 51 | * @param FormInterface $form 52 | * 53 | * @throws FormInvalidException 54 | * 55 | * @return mixed 56 | */ 57 | public function process(Request $request, FormInterface $form) 58 | { 59 | $data = json_decode($request->getContent(), true); 60 | 61 | $clearMissing = Request::METHOD_PATCH !== $request->getMethod(); 62 | 63 | $form->submit($data, $clearMissing); 64 | 65 | if ($form->isValid()) { 66 | return $this->onSuccess($form->getData()); 67 | } 68 | 69 | throw new FormInvalidException( 70 | ApiErrorEnumType::FORM_INVALID, 71 | 0, 72 | $this->formErrorsSerializer->serialize($form) 73 | ); 74 | } 75 | 76 | /** 77 | * @param mixed $object 78 | * 79 | * @return mixed 80 | */ 81 | abstract protected function onSuccess($object); 82 | } 83 | -------------------------------------------------------------------------------- /src/Form/Handler/DefaultFormHandler.php: -------------------------------------------------------------------------------- 1 | entityManager->persist($object); 17 | $this->entityManager->flush(); 18 | 19 | return $object; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Form/MovieType.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Form; 13 | 14 | use App\Entity\Movie; 15 | use Symfony\Component\Form\AbstractType; 16 | use Symfony\Component\Form\Extension\Core\Type\DateTimeType; 17 | use Symfony\Component\Form\FormBuilderInterface; 18 | use Symfony\Component\OptionsResolver\OptionsResolver; 19 | 20 | class MovieType extends AbstractType 21 | { 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function buildForm(FormBuilderInterface $builder, array $options) 26 | { 27 | $builder 28 | ->add('duration') 29 | ->add('title') 30 | ->add('description') 31 | ->add('director') 32 | ->add('publicationDate', DateTimeType::class, [ 33 | 'widget' => 'single_text', 34 | 'input' => 'datetime', 35 | ]); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function configureOptions(OptionsResolver $resolver) 42 | { 43 | $resolver->setDefaults([ 44 | 'data_class' => Movie::class, 45 | ]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Form/README.md: -------------------------------------------------------------------------------- 1 | # Event Forms and Filters 2 | 3 | `AbsctractFormHandler` is responsible for process your JSON payload, validation and returning possible errors. 4 | 5 | 6 | ## Forms 7 | 8 | Changes on Entity properties are possible because of Symfony Forms. 9 | 10 | You need to include all **required properties** otherwise **POST** will not pass validation. 11 | 12 | There is no need to include in **PATCH** payload all properties if you don't want to change them. 13 | 14 | 15 | ```php 16 | add('isbn') 21 | ->add('title') 22 | ->add('description'); 23 | ``` 24 | 25 | ## Filter 26 | 27 | Filter forms allow filtering results. It's a great tool - to get familiar with `FilterForms` check out [LexikFormFilterBundle Documentation](https://github.com/lexik/LexikFormFilterBundle/blob/master/Resources/doc/index.md) 28 | -------------------------------------------------------------------------------- /src/Form/ReviewType.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Form; 13 | 14 | use App\Entity\Review; 15 | use Symfony\Component\Form\AbstractType; 16 | use Symfony\Component\Form\Extension\Core\Type\DateTimeType; 17 | use Symfony\Component\Form\FormBuilderInterface; 18 | use Symfony\Component\OptionsResolver\OptionsResolver; 19 | 20 | class ReviewType extends AbstractType 21 | { 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function buildForm(FormBuilderInterface $builder, array $options) 26 | { 27 | $builder 28 | ->add('body') 29 | ->add('rating') 30 | ->add('author') 31 | ->add('publicationDate', DateTimeType::class, [ 32 | 'widget' => 'single_text', 33 | 'input' => 'datetime', 34 | ]); 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function configureOptions(OptionsResolver $resolver) 41 | { 42 | $resolver->setDefaults([ 43 | 'data_class' => Review::class, 44 | ]); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Form/UserType.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Form; 13 | 14 | use App\Entity\Book; 15 | use App\Entity\Movie; 16 | use App\Entity\Review; 17 | use App\Entity\User; 18 | use Doctrine\ORM\EntityRepository; 19 | use Symfony\Bridge\Doctrine\Form\Type\EntityType; 20 | use Symfony\Component\Form\AbstractType; 21 | use Symfony\Component\Form\FormBuilderInterface; 22 | use Symfony\Component\OptionsResolver\OptionsResolver; 23 | 24 | class UserType extends AbstractType 25 | { 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function buildForm(FormBuilderInterface $builder, array $options) 30 | { 31 | $builder 32 | ->add('fullName') 33 | ->add('email') 34 | ->add('plainPassword') 35 | ->add('books', EntityType::class, [ 36 | 'class' => Book::class, 37 | 'multiple' => 'true', 38 | 'query_builder' => function (EntityRepository $entityRepository) { 39 | return $entityRepository->createQueryBuilder('this'); 40 | }, 41 | ]) 42 | ->add('movies', EntityType::class, [ 43 | 'class' => Movie::class, 44 | 'multiple' => 'true', 45 | 'query_builder' => function (EntityRepository $entityRepository) { 46 | return $entityRepository->createQueryBuilder('this'); 47 | }, 48 | ]) 49 | ->add('reviews', EntityType::class, [ 50 | 'class' => Review::class, 51 | 'multiple' => 'true', 52 | 'query_builder' => function (EntityRepository $entityRepository) { 53 | return $entityRepository->createQueryBuilder('this'); 54 | }, 55 | ]); 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function configureOptions(OptionsResolver $resolver) 62 | { 63 | $resolver->setDefaults([ 64 | 'data_class' => User::class, 65 | ]); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Interfaces/ControllerInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Interfaces; 13 | 14 | use JMS\Serializer\SerializationContext; 15 | use Pagerfanta\Pagerfanta; 16 | use Symfony\Component\Form\FormInterface; 17 | use Symfony\Component\HttpFoundation\JsonResponse; 18 | use Symfony\Component\HttpFoundation\Request; 19 | use Symfony\Component\HttpFoundation\Response; 20 | 21 | interface ControllerInterface 22 | { 23 | /** 24 | * @param Pagerfanta $paginator 25 | * 26 | * @return JsonResponse 27 | */ 28 | public function createCollectionResponse(PagerFanta $paginator): JsonResponse; 29 | 30 | /** 31 | * @param $resource 32 | * @param int $status 33 | * @param SerializationContext|null $context 34 | * 35 | * @return JsonResponse 36 | */ 37 | public function createResourceResponse($resource, $status = Response::HTTP_OK, SerializationContext $context = null): JsonResponse; 38 | 39 | /** 40 | * @param array $data 41 | * @param int $status 42 | * 43 | * @return mixed 44 | */ 45 | public function createSuccessfulApiResponse(array $data, $status = Response::HTTP_OK); 46 | 47 | /** 48 | * @return JsonResponse 49 | */ 50 | public function createNotFoundResponse(): JsonResponse; 51 | 52 | /** 53 | * @param \Exception $exception 54 | * 55 | * @return JsonResponse 56 | */ 57 | public function createGenericErrorResponse(\Exception $exception): JsonResponse; 58 | 59 | /** 60 | * @param Request $request 61 | * @param string $filterForm 62 | * 63 | * @return Pagerfanta 64 | */ 65 | public function handleFilterForm(Request $request, string $filterForm): Pagerfanta; 66 | 67 | /** 68 | * @param string $type 69 | * @param array|null $data 70 | * @param array $options 71 | * 72 | * @return FormInterface 73 | */ 74 | public function getForm(string $type, $data = null, array $options = []): FormInterface; 75 | } 76 | -------------------------------------------------------------------------------- /src/Interfaces/RepositoryInterface.php: -------------------------------------------------------------------------------- 1 | getEnvironment(); 22 | } 23 | 24 | public function getLogDir() 25 | { 26 | return $this->getProjectDir().'/var/log'; 27 | } 28 | 29 | public function registerBundles() 30 | { 31 | $contents = include $this->getProjectDir().'/config/bundles.php'; 32 | foreach ($contents as $class => $envs) { 33 | if (isset($envs['all']) || isset($envs[$this->environment])) { 34 | yield new $class(); 35 | } 36 | } 37 | } 38 | 39 | protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader) 40 | { 41 | $container->setParameter('container.autowiring.strict_mode', true); 42 | $container->setParameter('container.dumper.inline_class_loader', true); 43 | $confDir = $this->getProjectDir().'/config'; 44 | $loader->load($confDir.'/packages/*'.self::CONFIG_EXTS, 'glob'); 45 | if (is_dir($confDir.'/packages/'.$this->environment)) { 46 | $loader->load($confDir.'/packages/'.$this->environment.'/**/*'.self::CONFIG_EXTS, 'glob'); 47 | } 48 | $loader->load($confDir.'/services'.self::CONFIG_EXTS, 'glob'); 49 | $loader->load($confDir.'/services_'.$this->environment.self::CONFIG_EXTS, 'glob'); 50 | } 51 | 52 | protected function configureRoutes(RouteCollectionBuilder $routes) 53 | { 54 | $confDir = $this->getProjectDir().'/config'; 55 | if (is_dir($confDir.'/routes/')) { 56 | $routes->import($confDir.'/routes/*'.self::CONFIG_EXTS, '/', 'glob'); 57 | } 58 | if (is_dir($confDir.'/routes/'.$this->environment)) { 59 | $routes->import($confDir.'/routes/'.$this->environment.'/**/*'.self::CONFIG_EXTS, '/', 'glob'); 60 | } 61 | $routes->import($confDir.'/routes'.self::CONFIG_EXTS, '/', 'glob'); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Migrations/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/src/Migrations/.gitignore -------------------------------------------------------------------------------- /src/Repository/AbstractRepository.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Repository; 13 | 14 | use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; 15 | use Doctrine\ORM\QueryBuilder; 16 | 17 | abstract class AbstractRepository extends ServiceEntityRepository 18 | { 19 | /** 20 | * @return QueryBuilder 21 | */ 22 | public function getQueryBuilder(): QueryBuilder 23 | { 24 | $qb = $this->createQueryBuilder('this')->select('this'); 25 | 26 | return $qb; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Repository/BookRepository.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Repository; 13 | 14 | use App\Entity\Book; 15 | use App\Interfaces\RepositoryInterface; 16 | use Symfony\Bridge\Doctrine\RegistryInterface; 17 | 18 | /** 19 | * {@inheritdoc} 20 | * 21 | * @method Book find($id, $lockMode = null, $lockVersion = null) 22 | * @method Book findOneBy(array $criteria, array $orderBy = null) 23 | * @method Book[] findAll() 24 | */ 25 | class BookRepository extends AbstractRepository implements RepositoryInterface 26 | { 27 | /** 28 | * BookRepository constructor. 29 | * 30 | * @param RegistryInterface $registry 31 | */ 32 | public function __construct(RegistryInterface $registry) 33 | { 34 | parent::__construct($registry, Book::class); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Repository/MovieRepository.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Repository; 13 | 14 | use App\Entity\Movie; 15 | use App\Interfaces\RepositoryInterface; 16 | use Symfony\Bridge\Doctrine\RegistryInterface; 17 | 18 | /** 19 | * {@inheritdoc} 20 | * 21 | * @method Movie find($id, $lockMode = null, $lockVersion = null) 22 | * @method Movie findOneBy(array $criteria, array $orderBy = null) 23 | * @method Movie[] findAll() 24 | */ 25 | class MovieRepository extends AbstractRepository implements RepositoryInterface 26 | { 27 | /** 28 | * BookRepository constructor. 29 | * 30 | * @param RegistryInterface $registry 31 | */ 32 | public function __construct(RegistryInterface $registry) 33 | { 34 | parent::__construct($registry, Movie::class); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Repository/README.md: -------------------------------------------------------------------------------- 1 | # Repositories 2 | -------------------------------------------------------------------------------- /src/Repository/ReviewRepository.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Repository; 13 | 14 | use App\Entity\Review; 15 | use App\Interfaces\RepositoryInterface; 16 | use Symfony\Bridge\Doctrine\RegistryInterface; 17 | 18 | /** 19 | * {@inheritdoc} 20 | * 21 | * @method Review find($id, $lockMode = null, $lockVersion = null) 22 | * @method Review findOneBy(array $criteria, array $orderBy = null) 23 | * @method Review[] findAll() 24 | */ 25 | class ReviewRepository extends AbstractRepository implements RepositoryInterface 26 | { 27 | /** 28 | * ReviewRepository constructor. 29 | * 30 | * @param RegistryInterface $registry 31 | */ 32 | public function __construct(RegistryInterface $registry) 33 | { 34 | parent::__construct($registry, Review::class); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Repository/UserRepository.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Repository; 13 | 14 | use App\Entity\User; 15 | use App\Interfaces\RepositoryInterface; 16 | use Symfony\Bridge\Doctrine\RegistryInterface; 17 | 18 | /** 19 | * {@inheritdoc} 20 | * 21 | * @method User find($id, $lockMode = null, $lockVersion = null) 22 | * @method User findOneBy(array $criteria, array $orderBy = null) 23 | * @method User[] findAll() 24 | */ 25 | class UserRepository extends AbstractRepository implements RepositoryInterface 26 | { 27 | /** 28 | * UserRepository constructor. 29 | * 30 | * @param RegistryInterface $registry 31 | */ 32 | public function __construct(RegistryInterface $registry) 33 | { 34 | parent::__construct($registry, User::class); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Resource/PaginationResource.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Resource; 13 | 14 | use Pagerfanta\Pagerfanta; 15 | 16 | final class PaginationResource 17 | { 18 | /** 19 | * @var int 20 | */ 21 | private $totalNumberOfResults; 22 | 23 | /** 24 | * @var int 25 | */ 26 | private $resultsPerPageCount; 27 | 28 | /** 29 | * @var int 30 | */ 31 | private $currentPageNumber; 32 | 33 | /** 34 | * PaginationResource constructor. 35 | * 36 | * @param int $totalNumberOfResults 37 | * @param int $resultsPerPageCount 38 | * @param int $currentPageNumber 39 | */ 40 | public function __construct(int $totalNumberOfResults = 0, int $resultsPerPageCount = 0, int $currentPageNumber = 0) 41 | { 42 | $this->totalNumberOfResults = $totalNumberOfResults; 43 | $this->resultsPerPageCount = $resultsPerPageCount; 44 | $this->currentPageNumber = $currentPageNumber; 45 | } 46 | 47 | /** 48 | * @param Pagerfanta $paginator 49 | * 50 | * @return self 51 | */ 52 | public static function createFromPagerfanta(Pagerfanta $paginator): self 53 | { 54 | return new self( 55 | $paginator->getNbResults(), 56 | $paginator->getMaxPerPage(), 57 | $paginator->getCurrentPage() 58 | ); 59 | } 60 | 61 | /** 62 | * @return array 63 | */ 64 | public function toJsArray(): array 65 | { 66 | return [ 67 | 'total' => $this->totalNumberOfResults, 68 | 'limit' => $this->resultsPerPageCount, 69 | 'page' => $this->currentPageNumber, 70 | ]; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Resource/README.md: -------------------------------------------------------------------------------- 1 | # Resource 2 | 3 | `PaginationResource` creates a pagination resource allowing to create Response. 4 | 5 | Check out [Pagerfanta documentation](https://github.com/whiteoctober/Pagerfanta#usage) to get more familiar with the solution. 6 | -------------------------------------------------------------------------------- /src/Security/README.md: -------------------------------------------------------------------------------- 1 | # Security and Voters 2 | 3 | ## UserProvider 4 | `UserProvider` is an implementation of `UserProviderInterface` build based on Symfony [All about User Providers](https://symfony.com/doc/current/security/user_provider.html) documentation. 5 | 6 | ## Voters 7 | Symfony voters are the most granular and flexible way of checking permissions to perform an action by User. 8 | 9 | Check out []How to Use Voters to [Check User Permissions](https://symfony.com/doc/current/security/voters.html) to get more familiar with Voters. 10 | 11 | In Controllers, they check for permissions to perform a specific action. 12 | 13 | Take a look on `@Security("is_granted('CAN_CREATE_BOOK', book)")` annotation. 14 | 15 | ```php 16 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Security; 13 | 14 | use App\Entity\User; 15 | use App\Service\Manager\UserManager; 16 | use Symfony\Component\Security\Core\Exception\UnsupportedUserException; 17 | use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; 18 | use Symfony\Component\Security\Core\User\UserInterface; 19 | use Symfony\Component\Security\Core\User\UserProviderInterface; 20 | 21 | class UserProvider implements UserProviderInterface 22 | { 23 | /** 24 | * @var UserManager 25 | */ 26 | protected $userManager; 27 | 28 | /** 29 | * UserProvider constructor. 30 | * 31 | * @param UserManager $userManager 32 | */ 33 | public function __construct(UserManager $userManager) 34 | { 35 | $this->userManager = $userManager; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function loadUserByUsername($username) 42 | { 43 | $user = $this->findUser($username); 44 | 45 | if (false === !!$user) { 46 | throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username)); 47 | } 48 | 49 | return $user; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function refreshUser(UserInterface $user): ?UserInterface 56 | { 57 | /** @var User $user */ 58 | if (!$this->supportsClass(\get_class($user))) { 59 | throw new UnsupportedUserException(sprintf('Expected an instance of %s, but got "%s".', $this->userManager->getClass(), \get_class($user))); 60 | } 61 | 62 | if (null === $reloadedUser = $this->userManager->findUserBy(['id' => $user->getId()])) { 63 | throw new UsernameNotFoundException(sprintf('User with ID "%s" could not be reloaded.', $user->getId())); 64 | } 65 | 66 | if ($reloadedUser instanceof UserInterface) { 67 | return $reloadedUser; 68 | } 69 | 70 | return null; 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | public function supportsClass($class) 77 | { 78 | $userClass = $this->userManager->getClass(); 79 | 80 | return $userClass === $class || is_subclass_of($class, $userClass); 81 | } 82 | 83 | /** 84 | * Finds a user by username. 85 | * 86 | * This method is meant to be an extension point for child classes. 87 | * 88 | * @param string $email 89 | * 90 | * @return UserInterface|null 91 | */ 92 | protected function findUser($email): ?UserInterface 93 | { 94 | return $this->userManager->findUserByEmail($email); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Security/Voter/Book/CreateBookVoter.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Security\Voter\Book; 13 | 14 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 15 | use Symfony\Component\Security\Core\Authorization\Voter\Voter; 16 | 17 | class CreateBookVoter extends Voter 18 | { 19 | public const CAN_CREATE_BOOK = 'CAN_CREATE_BOOK'; 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | protected function supports($attribute, $subject) 25 | { 26 | // you only want to vote if the attribute and subject are what you expect 27 | return self::CAN_CREATE_BOOK === $attribute && null === $subject; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | protected function voteOnAttribute($attribute, $subject, TokenInterface $token) 34 | { 35 | // our previous business logic indicates that mods and admins can do it regardless 36 | foreach ($token->getRoles() as $role) { 37 | if (\in_array($role->getRole(), ['ROLE_MODERATOR', 'ROLE_ADMIN'])) { 38 | return true; 39 | } 40 | } 41 | 42 | return false; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Security/Voter/Book/DeleteBookVoter.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Security\Voter\Book; 13 | 14 | use App\Entity\Book; 15 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 16 | use Symfony\Component\Security\Core\Authorization\Voter\Voter; 17 | 18 | class DeleteBookVoter extends Voter 19 | { 20 | public const CAN_DELETE_BOOK = 'CAN_DELETE_BOOK'; 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | protected function supports($attribute, $subject) 26 | { 27 | // you only want to vote if the attribute and subject are what you expect 28 | return self::CAN_DELETE_BOOK === $attribute && ($subject instanceof Book || null === $subject); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | protected function voteOnAttribute($attribute, $subject, TokenInterface $token) 35 | { 36 | // our previous business logic indicates that admins can do it regardless 37 | foreach ($token->getRoles() as $role) { 38 | if (\in_array($role->getRole(), ['ROLE_ADMIN'])) { 39 | return true; 40 | } 41 | } 42 | 43 | return false; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Security/Voter/Book/UpdateBookVoter.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Security\Voter\Book; 13 | 14 | use App\Entity\Book; 15 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 16 | use Symfony\Component\Security\Core\Authorization\Voter\Voter; 17 | 18 | class UpdateBookVoter extends Voter 19 | { 20 | public const CAN_UPDATE_BOOK = 'CAN_UPDATE_BOOK'; 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | protected function supports($attribute, $subject) 26 | { 27 | // you only want to vote if the attribute and subject are what you expect 28 | return self::CAN_UPDATE_BOOK === $attribute && ($subject instanceof Book || null === $subject); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | protected function voteOnAttribute($attribute, $subject, TokenInterface $token) 35 | { 36 | // our previous business logic indicates that mods and admins can do it regardless 37 | foreach ($token->getRoles() as $role) { 38 | if (\in_array($role->getRole(), ['ROLE_MODERATOR', 'ROLE_ADMIN'])) { 39 | return true; 40 | } 41 | } 42 | 43 | return false; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Security/Voter/Movie/CreateMovieVoter.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Security\Voter\Movie; 13 | 14 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 15 | use Symfony\Component\Security\Core\Authorization\Voter\Voter; 16 | 17 | class CreateMovieVoter extends Voter 18 | { 19 | public const CAN_CREATE_MOVIE = 'CAN_CREATE_MOVIE'; 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | protected function supports($attribute, $subject) 25 | { 26 | // you only want to vote if the attribute and subject are what you expect 27 | return self::CAN_CREATE_MOVIE === $attribute && null === $subject; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | protected function voteOnAttribute($attribute, $subject, TokenInterface $token) 34 | { 35 | // our previous business logic indicates that mods and admins can do it regardless 36 | foreach ($token->getRoles() as $role) { 37 | if (\in_array($role->getRole(), ['ROLE_MODERATOR', 'ROLE_ADMIN'])) { 38 | return true; 39 | } 40 | } 41 | 42 | return false; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Security/Voter/Movie/DeleteMovieVoter.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Security\Voter\Movie; 13 | 14 | use App\Entity\Movie; 15 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 16 | use Symfony\Component\Security\Core\Authorization\Voter\Voter; 17 | 18 | class DeleteMovieVoter extends Voter 19 | { 20 | public const CAN_DELETE_MOVIE = 'CAN_DELETE_MOVIE'; 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | protected function supports($attribute, $subject) 26 | { 27 | // you only want to vote if the attribute and subject are what you expect 28 | return self::CAN_DELETE_MOVIE === $attribute && ($subject instanceof Movie || null === $subject); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | protected function voteOnAttribute($attribute, $subject, TokenInterface $token) 35 | { 36 | // our previous business logic indicates that admins can do it regardless 37 | foreach ($token->getRoles() as $role) { 38 | if (\in_array($role->getRole(), ['ROLE_ADMIN'])) { 39 | return true; 40 | } 41 | } 42 | 43 | return false; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Security/Voter/Movie/UpdateMovieVoter.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Security\Voter\Movie; 13 | 14 | use App\Entity\Movie; 15 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 16 | use Symfony\Component\Security\Core\Authorization\Voter\Voter; 17 | 18 | class UpdateMovieVoter extends Voter 19 | { 20 | public const CAN_UPDATE_MOVIE = 'CAN_UPDATE_MOVIE'; 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | protected function supports($attribute, $subject) 26 | { 27 | // you only want to vote if the attribute and subject are what you expect 28 | return self::CAN_UPDATE_MOVIE === $attribute && ($subject instanceof Movie || null === $subject); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | protected function voteOnAttribute($attribute, $subject, TokenInterface $token) 35 | { 36 | // our previous business logic indicates that mods and admins can do it regardless 37 | foreach ($token->getRoles() as $role) { 38 | if (\in_array($role->getRole(), ['ROLE_MODERATOR', 'ROLE_ADMIN'])) { 39 | return true; 40 | } 41 | } 42 | 43 | return false; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Security/Voter/Review/CreateReviewVoter.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Security\Voter\Review; 13 | 14 | use App\Entity\User; 15 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 16 | use Symfony\Component\Security\Core\Authorization\Voter\Voter; 17 | 18 | class CreateReviewVoter extends Voter 19 | { 20 | public const CAN_CREATE_REVIEW = 'CAN_CREATE_REVIEW'; 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | protected function supports($attribute, $subject) 26 | { 27 | // you only want to vote if the attribute and subject are what you expect 28 | return self::CAN_CREATE_REVIEW === $attribute && null === $subject; 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | protected function voteOnAttribute($attribute, $subject, TokenInterface $token) 35 | { 36 | $user = $token->getUser(); 37 | 38 | // allow user to delete account 39 | if ($user instanceof User) { 40 | return true; 41 | } 42 | 43 | return false; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Security/Voter/Review/DeleteReviewVoter.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Security\Voter\Review; 13 | 14 | use App\Entity\Review; 15 | use App\Entity\User; 16 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 17 | use Symfony\Component\Security\Core\Authorization\Voter\Voter; 18 | 19 | class DeleteReviewVoter extends Voter 20 | { 21 | public const CAN_DELETE_REVIEW = 'CAN_DELETE_REVIEW'; 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | protected function supports($attribute, $subject) 27 | { 28 | // you only want to vote if the attribute and subject are what you expect 29 | return self::CAN_DELETE_REVIEW === $attribute && ($subject instanceof Review || null === $subject); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | protected function voteOnAttribute($attribute, $subject, TokenInterface $token) 36 | { 37 | // our previous business logic indicates that admins can do it regardless 38 | foreach ($token->getRoles() as $role) { 39 | if (\in_array($role->getRole(), ['ROLE_ADMIN'])) { 40 | return true; 41 | } 42 | } 43 | 44 | // allow controller handle not found subject 45 | if (null === $subject) { 46 | return true; 47 | } 48 | 49 | $user = $token->getUser(); 50 | 51 | // allow user to delete account 52 | if ($user instanceof User) { 53 | return $subject->getAuthor()->getId() === $user->getId(); 54 | } 55 | 56 | return false; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Security/Voter/Review/UpdateReviewVoter.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Security\Voter\Review; 13 | 14 | use App\Entity\Review; 15 | use App\Entity\User; 16 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 17 | use Symfony\Component\Security\Core\Authorization\Voter\Voter; 18 | 19 | class UpdateReviewVoter extends Voter 20 | { 21 | public const CAN_UPDATE_REVIEW = 'CAN_UPDATE_REVIEW'; 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | protected function supports($attribute, $subject) 27 | { 28 | // you only want to vote if the attribute and subject are what you expect 29 | return self::CAN_UPDATE_REVIEW === $attribute && ($subject instanceof Review || null === $subject); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | protected function voteOnAttribute($attribute, $subject, TokenInterface $token) 36 | { 37 | // our previous business logic indicates that mods and admins can do it regardless 38 | foreach ($token->getRoles() as $role) { 39 | if (\in_array($role->getRole(), ['ROLE_MODERATOR', 'ROLE_ADMIN'])) { 40 | return true; 41 | } 42 | } 43 | 44 | // allow controller handle not found subject 45 | if (null === $subject) { 46 | return true; 47 | } 48 | 49 | $user = $token->getUser(); 50 | 51 | // allow user to update account 52 | if ($user instanceof User) { 53 | return $subject->getAuthor()->getId() === $user->getId(); 54 | } 55 | 56 | return false; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Security/Voter/User/DeleteUserVoter.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Security\Voter\User; 13 | 14 | use App\Entity\User; 15 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 16 | use Symfony\Component\Security\Core\Authorization\Voter\Voter; 17 | 18 | class DeleteUserVoter extends Voter 19 | { 20 | public const CAN_DELETE_USER = 'CAN_DELETE_USER'; 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | protected function supports($attribute, $subject) 26 | { 27 | // you only want to vote if the attribute and subject are what you expect 28 | return self::CAN_DELETE_USER === $attribute && ($subject instanceof User || null === $subject); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | protected function voteOnAttribute($attribute, $subject, TokenInterface $token) 35 | { 36 | // our previous business logic indicates that admins can do it regardless 37 | foreach ($token->getRoles() as $role) { 38 | if (\in_array($role->getRole(), ['ROLE_ADMIN'])) { 39 | return true; 40 | } 41 | } 42 | 43 | // allow controller handle not found subject 44 | if (null === $subject) { 45 | return true; 46 | } 47 | 48 | $user = $token->getUser(); 49 | 50 | // allow user to delete account 51 | if ($user instanceof User) { 52 | return $subject->getId() === $user->getId(); 53 | } 54 | 55 | return false; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Security/Voter/User/UpdateUserVoter.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Security\Voter\User; 13 | 14 | use App\Entity\User; 15 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 16 | use Symfony\Component\Security\Core\Authorization\Voter\Voter; 17 | 18 | class UpdateUserVoter extends Voter 19 | { 20 | public const CAN_UPDATE_USER = 'CAN_UPDATE_USER'; 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | protected function supports($attribute, $subject) 26 | { 27 | // you only want to vote if the attribute and subject are what you expect 28 | return self::CAN_UPDATE_USER === $attribute && ($subject instanceof User || null === $subject); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | protected function voteOnAttribute($attribute, $subject, TokenInterface $token) 35 | { 36 | // our previous business logic indicates that mods and admins can do it regardless 37 | foreach ($token->getRoles() as $role) { 38 | if (\in_array($role->getRole(), ['ROLE_MODERATOR', 'ROLE_ADMIN'])) { 39 | return true; 40 | } 41 | } 42 | 43 | // allow controller handle not found subject 44 | if (null === $subject) { 45 | return true; 46 | } 47 | 48 | $user = $token->getUser(); 49 | 50 | // allow user to update account 51 | if ($user instanceof User) { 52 | return $subject->getId() === $user->getId(); 53 | } 54 | 55 | return false; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Service/Form/FormErrorsSerializer.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Service\Form; 13 | 14 | use Symfony\Component\Form\FormInterface; 15 | 16 | class FormErrorsSerializer 17 | { 18 | /** 19 | * @var array 20 | */ 21 | protected $errors; 22 | 23 | /** 24 | * @param FormInterface $form 25 | * 26 | * @return array 27 | */ 28 | public function serialize(FormInterface $form): array 29 | { 30 | $this->errors = $this->serializeErrors($form); 31 | 32 | return $this->errors; 33 | } 34 | 35 | /** 36 | * @param FormInterface $form 37 | * 38 | * @return array 39 | */ 40 | protected function serializeErrors(FormInterface $form): array 41 | { 42 | $errors = []; 43 | foreach ($form->getErrors() as $error) { 44 | $errors[] = $error->getMessage(); 45 | } 46 | 47 | foreach ($form->all() as $childForm) { 48 | if ($childForm instanceof FormInterface) { 49 | if ($childErrors = $this->serializeErrors($childForm)) { 50 | $errors[$childForm->getName()] = $childErrors; 51 | } 52 | } 53 | } 54 | 55 | return $errors; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Service/Generic/ResponseCreator.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Service\Generic; 13 | 14 | use App\Resource\PaginationResource; 15 | use Doctrine\Common\Persistence\ObjectManager; 16 | use JMS\Serializer\SerializerInterface; 17 | use Symfony\Component\HttpFoundation\JsonResponse; 18 | 19 | class ResponseCreator 20 | { 21 | /** 22 | * @var ObjectManager 23 | */ 24 | protected $objectManager; 25 | 26 | /** 27 | * @var SerializerInterface 28 | */ 29 | protected $serializer; 30 | 31 | /** 32 | * @var SerializationService 33 | */ 34 | protected $serializationService; 35 | 36 | /** 37 | * @var array 38 | */ 39 | protected $data; 40 | 41 | /** 42 | * @var PaginationResource 43 | */ 44 | protected $pagination; 45 | 46 | /** 47 | * ResponseCreator constructor. 48 | * 49 | * @param ObjectManager $objectManager 50 | * @param SerializationService $serializationService 51 | * @param SerializerInterface $serializer 52 | */ 53 | public function __construct( 54 | ObjectManager $objectManager, 55 | SerializationService $serializationService, 56 | SerializerInterface $serializer 57 | ) { 58 | $this->objectManager = $objectManager; 59 | $this->serializationService = $serializationService; 60 | $this->serializer = $serializer; 61 | $this->data = []; 62 | $this->pagination = null; 63 | } 64 | 65 | /** 66 | * @param array $data 67 | */ 68 | public function setData(array $data): void 69 | { 70 | $this->data = $data; 71 | } 72 | 73 | /** 74 | * @param array $data 75 | * @param paginationResource $pagination 76 | */ 77 | public function setCollectionData(array $data, PaginationResource $pagination): void 78 | { 79 | $this->data = $data; 80 | $this->pagination = $pagination; 81 | } 82 | 83 | /** 84 | * @param int $code 85 | * @param string $className 86 | * 87 | * @return JsonResponse 88 | */ 89 | public function getResponse(int $code, string $className = null): JsonResponse 90 | { 91 | $context = $this->serializationService->createBaseOnRequest(); 92 | 93 | $response = new JsonResponse(null, $code); 94 | $response->setContent($this->serializer->serialize($this->buildResponse($className), 'json', $context)); 95 | 96 | return $response; 97 | } 98 | 99 | /** 100 | * @param string $className 101 | * 102 | * @return array 103 | */ 104 | protected function buildResponse(string $className = null): ?array 105 | { 106 | if (null === $className) { 107 | return $this->data; 108 | } 109 | 110 | $responseArray = []; 111 | $responseArray[$className] = $this->data ?: new \stdClass(); 112 | 113 | if (null !== $this->pagination) { 114 | $responseArray['pagination'] = $this->pagination->toJsArray(); 115 | } 116 | 117 | return $responseArray; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Service/Generic/SerializationService.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Service\Generic; 13 | 14 | use JMS\Serializer\SerializationContext; 15 | use Symfony\Component\HttpFoundation\RequestStack; 16 | 17 | class SerializationService 18 | { 19 | /** 20 | * @var RequestStack 21 | */ 22 | private $requestStack; 23 | 24 | /** 25 | * SerializationService constructor. 26 | * 27 | * @param RequestStack $requestStack 28 | */ 29 | public function __construct(RequestStack $requestStack) 30 | { 31 | $this->requestStack = $requestStack; 32 | } 33 | 34 | /** 35 | * @return SerializationContext|null 36 | */ 37 | public function createBaseOnRequest(): SerializationContext 38 | { 39 | $currentRequest = $this->requestStack->getCurrentRequest(); 40 | 41 | if (false === !!$currentRequest) { 42 | throw new \RuntimeException(sprintf('Current request is is required!')); 43 | } 44 | 45 | $expand = $currentRequest 46 | ->query 47 | ->get('expand', []); 48 | 49 | if (true === \is_string($expand)) { 50 | $expand = explode(',', $expand); 51 | } 52 | 53 | return $this->createWithGroups($expand); 54 | } 55 | 56 | /** 57 | * @param array $groups 58 | * 59 | * @return SerializationContext 60 | */ 61 | public function createWithGroups(array $groups): SerializationContext 62 | { 63 | $serializationContext = (new SerializationContext())->create(); 64 | $serializationContext->setGroups(array_merge(['Default'], $groups)); 65 | 66 | return $serializationContext; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Service/Manager/UserManager.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Service\Manager; 13 | 14 | use App\Entity\User; 15 | use App\Repository\UserRepository; 16 | use Doctrine\ORM\EntityManagerInterface; 17 | use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; 18 | use Symfony\Component\Security\Core\User\UserInterface; 19 | 20 | class UserManager 21 | { 22 | /** 23 | * @var EncoderFactoryInterface 24 | */ 25 | protected $encoderFactory; 26 | /** 27 | * @var EntityManagerInterface 28 | */ 29 | protected $entityManager; 30 | 31 | /** 32 | * @var UserRepository 33 | */ 34 | protected $repository; 35 | 36 | /** 37 | * UserManager constructor. 38 | * 39 | * @param EncoderFactoryInterface $encoderFactory 40 | * @param EntityManagerInterface $entityManager 41 | * @param UserRepository $userRepository 42 | */ 43 | public function __construct( 44 | EncoderFactoryInterface $encoderFactory, 45 | EntityManagerInterface $entityManager, 46 | UserRepository $userRepository 47 | ) { 48 | $this->encoderFactory = $encoderFactory; 49 | $this->entityManager = $entityManager; 50 | $this->repository = $userRepository; 51 | } 52 | 53 | /** 54 | * @param UserInterface $user 55 | */ 56 | public function deleteUser(UserInterface $user) 57 | { 58 | $this->entityManager->remove($user); 59 | $this->entityManager->flush(); 60 | } 61 | 62 | /** 63 | * @return string 64 | */ 65 | public function getClass() 66 | { 67 | return User::class; 68 | } 69 | 70 | /** 71 | * @param array $criteria 72 | * 73 | * @return object 74 | */ 75 | public function findUserBy(array $criteria) 76 | { 77 | return $this->repository->findOneBy($criteria); 78 | } 79 | 80 | /** 81 | * @return array 82 | */ 83 | public function findUsers() 84 | { 85 | return $this->repository->findAll(); 86 | } 87 | 88 | /** 89 | * @param UserInterface $user 90 | */ 91 | public function reloadUser(UserInterface $user) 92 | { 93 | $this->entityManager->refresh($user); 94 | } 95 | 96 | /** 97 | * Finds a user by email. 98 | * 99 | * @param string $email 100 | * 101 | * @return object|User|UserInterface 102 | */ 103 | public function findUserByEmail($email) 104 | { 105 | return $this->findUserBy(['email' => strtolower($email)]); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Service/README.md: -------------------------------------------------------------------------------- 1 | # Services 2 | 3 | ## Form 4 | 5 | `FormErrorSerializer` is responsible for serialization error form submitted form. 6 | 7 | ## Generic 8 | 9 | `SerializationService` provides serialization context. 10 | 11 | ## Manager 12 | 13 | `UserManager` is a simple service allowing to manage users. It also provide `UserProvider` with necessary methods. 14 | -------------------------------------------------------------------------------- /src/Traits/ControllerTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Traits; 13 | 14 | use App\Form\Handler\DefaultFormHandler; 15 | use App\Service\Generic\ResponseCreator; 16 | use App\Service\Generic\SerializationService; 17 | use Doctrine\Common\Inflector\Inflector; 18 | use JMS\Serializer\Serializer; 19 | use JMS\Serializer\SerializerInterface; 20 | use Symfony\Component\Form\FormFactoryInterface; 21 | 22 | trait ControllerTrait 23 | { 24 | /** 25 | * @var Inflector 26 | */ 27 | protected $inflector; 28 | 29 | /** 30 | * @var FormFactoryInterface 31 | */ 32 | protected $formFactory; 33 | 34 | /** 35 | * @var DefaultFormHandler 36 | */ 37 | protected $formHandler; 38 | 39 | /** 40 | * @var ResponseCreator 41 | */ 42 | protected $responseCreator; 43 | 44 | /** 45 | * @var Serializer 46 | */ 47 | protected $serializer; 48 | 49 | /** 50 | * @var SerializationService 51 | */ 52 | protected $serializationService; 53 | 54 | /** 55 | * @required 56 | * 57 | * @param Inflector $inflector 58 | */ 59 | public function setInflector(Inflector $inflector): void 60 | { 61 | $this->inflector = $inflector; 62 | } 63 | 64 | /** 65 | * @required 66 | * 67 | * @param FormFactoryInterface $formFactory 68 | */ 69 | public function setFormFactory(FormFactoryInterface $formFactory): void 70 | { 71 | $this->formFactory = $formFactory; 72 | } 73 | 74 | /** 75 | * @required 76 | * 77 | * @param DefaultFormHandler $formHandler 78 | */ 79 | public function setFormHandler(DefaultFormHandler $formHandler): void 80 | { 81 | $this->formHandler = $formHandler; 82 | } 83 | 84 | /** 85 | * @required 86 | * 87 | * @param ResponseCreator $responseCreator 88 | */ 89 | public function setResponseCreator(ResponseCreator $responseCreator): void 90 | { 91 | $this->responseCreator = $responseCreator; 92 | } 93 | 94 | /** 95 | * @required 96 | * 97 | * @param SerializerInterface $serializer 98 | */ 99 | public function setSerializer(SerializerInterface $serializer): void 100 | { 101 | if (!$serializer instanceof Serializer) { 102 | throw new \InvalidArgumentException( 103 | sprintf( 104 | 'Serializer must be instance of %s but %s given', 105 | Serializer::class, 106 | \get_class($this->serializer) 107 | ) 108 | ); 109 | } 110 | 111 | $this->serializer = $serializer; 112 | } 113 | 114 | /** 115 | * @required 116 | * 117 | * @param SerializationService $serializationService 118 | */ 119 | public function setSerializationService(SerializationService $serializationService): void 120 | { 121 | $this->serializationService = $serializationService; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Traits/IdColumnTrait.php: -------------------------------------------------------------------------------- 1 | id; 29 | } 30 | 31 | /** 32 | * @return int 33 | */ 34 | public function getIdOrThrow(): int 35 | { 36 | if (!$this->id) { 37 | throw new \RuntimeException(sprintf( 38 | 'Entity `%s` is not flushed, therefore its id is not known yet', 39 | __CLASS__ 40 | )); 41 | } 42 | 43 | return $this->id; 44 | } 45 | 46 | /** 47 | * @param int $id 48 | */ 49 | public function setId(int $id) 50 | { 51 | $this->id = $id; 52 | } 53 | 54 | public function resetId() 55 | { 56 | $this->id = null; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Traits/README.md: -------------------------------------------------------------------------------- 1 | # Traits 2 | 3 | `ControllerTrait` is the list of properties and setters. 4 | 5 | ```php 6 | inflector = $inflector; 25 | } 26 | ``` 27 | Setters have `@required` parameter when you use this trait all setters are executed and set traits properties. 28 | 29 | This trait is used to avoid passing a lot of parameters to `AbstractController.php` 30 | 31 | `IdCollumnTrait` and `TimeAwareTrait` allow to reduce repetitive code. 32 | -------------------------------------------------------------------------------- /src/Traits/TimeAwareTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Traits; 13 | 14 | use DateTimeInterface; 15 | use Doctrine\ORM\Mapping as ORM; 16 | use Gedmo\Mapping\Annotation as Gedmo; 17 | use JMS\Serializer\Annotation as JMS; 18 | 19 | trait TimeAwareTrait 20 | { 21 | /** 22 | * @var DateTimeInterface 23 | * 24 | * @JMS\Expose 25 | * 26 | * @Gedmo\Timestampable(on="create") 27 | * @ORM\Column(type="datetime") 28 | */ 29 | protected $created; 30 | 31 | /** 32 | * @var DateTimeInterface 33 | * 34 | * @JMS\Expose 35 | * 36 | * @Gedmo\Timestampable(on="update") 37 | * @ORM\Column(type="datetime") 38 | */ 39 | protected $updated; 40 | 41 | /** 42 | * Set created. 43 | * 44 | * @param DateTimeInterface|null $created 45 | * 46 | * @return mixed 47 | */ 48 | public function setCreated(DateTimeInterface $created = null) 49 | { 50 | $this->created = $created; 51 | 52 | return $this; 53 | } 54 | 55 | /** 56 | * Get created. 57 | * 58 | * @return DateTimeInterface 59 | */ 60 | public function getCreated(): DateTimeInterface 61 | { 62 | return $this->created; 63 | } 64 | 65 | /** 66 | * Set updated. 67 | * 68 | * @param DateTimeInterface|null $updated 69 | * 70 | * @return mixed 71 | */ 72 | public function setUpdated(DateTimeInterface $updated = null) 73 | { 74 | $this->updated = $updated; 75 | 76 | return $this; 77 | } 78 | 79 | /** 80 | * Get updated. 81 | * 82 | * @return DateTimeInterface $updated 83 | */ 84 | public function getUpdated(): DateTimeInterface 85 | { 86 | return $this->updated; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /templates/base.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Welcome!{% endblock %} 6 | {% block stylesheets %}{% endblock %} 7 | 8 | 9 | {% block body %}{% endblock %} 10 | {% block javascripts %}{% endblock %} 11 | 12 | 13 | -------------------------------------------------------------------------------- /templates/bundles/NelmioApiDocBundle/SwaggerUi/index.html.twig: -------------------------------------------------------------------------------- 1 | {# templates/bundles/NelmioApiDocBundle/SwaggerUi/index.html.twig #} 2 | 3 | {# 4 | To avoid a "reached nested level" error an exclamation mark `!` has to be added 5 | See https://symfony.com/blog/new-in-symfony-3-4-improved-the-overriding-of-templates 6 | #} 7 | {% extends '@!NelmioApiDoc/SwaggerUi/index.html.twig' %} 8 | 9 | {% block stylesheets %} 10 | {{ parent() }} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {% endblock stylesheets %} 31 | 32 | {% block javascripts %} 33 | {{ parent() }} 34 | 35 | {% endblock javascripts %} 36 | 37 | {% block header %} 38 | 39 | {% endblock header %} 40 | -------------------------------------------------------------------------------- /templates/bundles/TwigBundle/Exception/error404.html.twig: -------------------------------------------------------------------------------- 1 | {# templates/bundles/TwigBundle/Exception/error404.html.twig #} 2 | {% extends 'base.html.twig' %} 3 | 4 | {% block body %} 5 |

Page not found

6 | 7 |

8 | The requested page couldn't be located. Checkout for any URL 9 | misspelling or return to the homepage. 10 |

11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /templates/bundles/TwigBundle/Exception/error404.json.twig: -------------------------------------------------------------------------------- 1 | { 2 | "error": "404 Not found." 3 | } 4 | -------------------------------------------------------------------------------- /tests/Controller/AbstractWebTestCase.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare(strict_types=1); 11 | 12 | namespace App\Tests\Controller; 13 | 14 | use Faker\Factory; 15 | use Faker\Generator; 16 | use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTEncodeFailureException; 17 | use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; 18 | use Symfony\Component\BrowserKit\Client; 19 | 20 | abstract class AbstractWebTestCase extends WebTestCase 21 | { 22 | /** 23 | * @var Client 24 | */ 25 | protected $client; 26 | 27 | /** 28 | * @var string|null 29 | */ 30 | protected $token; 31 | 32 | /** 33 | * @var Generator 34 | */ 35 | protected $faker; 36 | 37 | /** 38 | * AbstractWebTestCase constructor. 39 | * 40 | * @param string|null $name 41 | * @param array $data 42 | * @param string $dataName 43 | */ 44 | public function __construct(?string $name = null, array $data = [], string $dataName = '') 45 | { 46 | parent::__construct($name, $data, $dataName); 47 | 48 | $this->client = static::createClient(); 49 | $this->token = self::getToken(); 50 | $this->faker = Factory::create(); 51 | } 52 | 53 | /** 54 | * @return string|null 55 | */ 56 | private static function getToken(): ?string 57 | { 58 | self::bootKernel(); 59 | 60 | // returns the real and unchanged service container 61 | $container = self::$kernel->getContainer(); 62 | 63 | $data = ['username' => 'developer@symfony.local', 'roles' => ['ROLE_ADMIN']]; 64 | 65 | try { 66 | $token = $container 67 | ->get('lexik_jwt_authentication.encoder') 68 | ->encode($data); 69 | } catch (JWTEncodeFailureException $e) { 70 | echo $e->getMessage().PHP_EOL; 71 | } 72 | 73 | return $token; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /translations/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/translations/.gitignore -------------------------------------------------------------------------------- /var/data.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tulik/symfony-4-rest-api/ad4b8aec093a3d188e9d47b9c586e0798d45d148/var/data.db --------------------------------------------------------------------------------