├── .env.dist ├── .gitignore ├── README.md ├── bin └── console ├── composer.json ├── composer.lock ├── config ├── bundles.php ├── packages │ ├── dev │ │ ├── routing.yaml │ │ ├── security_checker.yaml │ │ ├── swiftmailer.yaml │ │ └── web_profiler.yaml │ ├── doctrine.yaml │ ├── doctrine_migrations.yaml │ ├── fos_http_cache.yaml │ ├── fos_rest.yaml │ ├── framework.yaml │ ├── framework_extra.yaml │ ├── lexik_jwt_authentication.yaml │ ├── nelmio_api_doc.yaml │ ├── nelmio_cors.yaml │ ├── prod │ │ └── doctrine.yaml │ ├── routing.yaml │ ├── security.yaml │ ├── snc_redis.yaml │ ├── swiftmailer.yaml │ ├── test │ │ ├── framework.yaml │ │ ├── swiftmailer.yaml │ │ └── web_profiler.yaml │ ├── translation.yaml │ ├── twig.yaml │ └── twig_extensions.yaml ├── routes.yaml ├── routes │ ├── annotations.yaml │ └── dev │ │ ├── twig.yaml │ │ └── web_profiler.yaml └── services.yaml ├── public ├── index.php └── uploads │ └── images │ └── ecb66f083f14b45fe68dbf07caef9c5b.png ├── src ├── Annotation │ └── DeserializeEntity.php ├── CacheKernel.php ├── Controller │ ├── .gitignore │ ├── ExceptionController.php │ ├── ImagesController.php │ ├── MoviesController.php │ ├── PersonsController.php │ ├── TokensController.php │ └── UsersController.php ├── DataFixtures │ ├── .gitignore │ ├── Fixtures │ │ ├── movie.yaml │ │ ├── person.yaml │ │ └── role.yaml │ ├── LoadMovieData.php │ ├── LoadPersonData.php │ ├── LoadRoleData.php │ └── LoadUserData.php ├── Entity │ ├── .gitignore │ ├── EntityMerger.php │ ├── Image.php │ ├── Movie.php │ ├── Person.php │ ├── Role.php │ └── User.php ├── Exception │ └── ValidationException.php ├── Kernel.php ├── Migrations │ └── .gitignore ├── Repository │ ├── .gitignore │ ├── ImageRepository.php │ ├── MovieRepository.php │ ├── PersonRepository.php │ ├── RoleRepository.php │ └── UserRepository.php ├── Resource │ ├── Filtering │ │ ├── AbstractFilterDefinition.php │ │ ├── AbstractFilterDefinitionFactory.php │ │ ├── FilterDefinitionFactoryInterface.php │ │ ├── FilterDefinitionInterface.php │ │ ├── Movie │ │ │ ├── MovieFilterDefinition.php │ │ │ ├── MovieFilterDefinitionFactory.php │ │ │ └── MovieResourceFilter.php │ │ ├── Person │ │ │ ├── PersonFilterDefinition.php │ │ │ ├── PersonFilterDefinitionFactory.php │ │ │ └── PersonResourceFilter.php │ │ ├── ResourceFilterInterface.php │ │ ├── Role │ │ │ ├── RoleFilterDefinition.php │ │ │ ├── RoleFilterDefinitionFactory.php │ │ │ └── RoleResourceFilter.php │ │ └── SortTableFilterDefinitionInterface.php │ └── Pagination │ │ ├── AbstractPagination.php │ │ ├── Movie │ │ └── MoviePagination.php │ │ ├── Page.php │ │ ├── PageRequestFactory.php │ │ ├── PaginationInterface.php │ │ ├── Person │ │ └── PersonPagination.php │ │ └── Role │ │ └── RolePagination.php ├── Security │ ├── TokenAuthenticator.php │ ├── TokenStorage.php │ └── UserVoter.php └── Serializer │ └── DoctrineEntityDeserializationSubscriber.php ├── symfony.lock ├── templates └── base.html.twig └── translations └── .gitignore /.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=dev 7 | APP_SECRET=d7a9a96d614047337d730fab4448008b 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=mysql://db_user:db_password@127.0.0.1:3306/db_name 17 | ###< doctrine/doctrine-bundle ### 18 | # DATABASE_URL=pgsql://db_user:db_password@127.0.0.1:5432/db_name 19 | 20 | ###> symfony/swiftmailer-bundle ### 21 | # For Gmail as a transport, use: "gmail://username:password@localhost" 22 | # For a generic SMTP server, use: "smtp://localhost:25?encryption=&auth_mode=" 23 | # Delivery is disabled by default via "null://localhost" 24 | MAILER_URL=null://localhost 25 | ###< symfony/swiftmailer-bundle ### 26 | 27 | ###> lexik/jwt-authentication-bundle ### 28 | # $ mkdir -p config/jwt # For Symfony3+, no need of the -p option 29 | # $ openssl genrsa -out config/jwt/private.pem -aes256 4096 30 | # $ openssl rsa -pubout -in config/jwt/private.pem -out config/jwt/public.pem 31 | # Key paths should be relative to the project directory 32 | JWT_PRIVATE_KEY_PATH=config/jwt/private.pem 33 | JWT_PUBLIC_KEY_PATH=config/jwt/public.pem 34 | JWT_PASSPHRASE=182a5c4a452bcfd3c9d55f6e4bd0ce41 35 | ###< lexik/jwt-authentication-bundle ### 36 | 37 | ###> nelmio/cors-bundle ### 38 | CORS_ALLOW_ORIGIN=^https?://localhost:?[0-9]*$ 39 | ###< nelmio/cors-bundle ### 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ###> symfony/framework-bundle ### 3 | /.env 4 | /public/bundles/ 5 | /var/ 6 | /vendor/ 7 | ###< symfony/framework-bundle ### 8 | .idea/ 9 | ###> symfony/web-server-bundle ### 10 | /.web-server-pid 11 | ###< symfony/web-server-bundle ### 12 | 13 | ###> lexik/jwt-authentication-bundle ### 14 | /config/jwt/*.pem 15 | ###< lexik/jwt-authentication-bundle ### 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rest Api Symfony Application 2 | ======================== 3 | 4 | Requirements 5 | ------------ 6 | 7 | * PHP 7.1.3 or higher; 8 | * PDO-pgSQL PHP extension enabled; 9 | * Redis; 10 | * and the [usual Symfony application requirements][1]. 11 | 12 | Installation 13 | ------------ 14 | 15 | Execute this command to install the project: 16 | 17 | ```bash 18 | $ git clone git@github.com:neverovski/restapisymfony.git 19 | $ cd restapisymfony 20 | $ composer install 21 | ``` 22 | Database create and migration 23 | ----------------------------- 24 | ```bash 25 | $ php bin/console doctrine:database:create 26 | $ php bin/console doctrine:migrations:diff 27 | $ php bin/console doctrine:migrations:migrate 28 | ``` 29 | 30 | Usage 31 | ----- 32 | 33 | There's no need to configure anything to run the application. Just execute this 34 | command to run the built-in web server and access the application in your 35 | browser at : 36 | 37 | ```bash 38 | $ php bin/console server:run 39 | ``` 40 | 41 | Alternatively, you can [configure a fully-featured web server][2] like Nginx 42 | or Apache to run the application. 43 | -------------------------------------------------------------------------------- /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', true); 27 | $debug = (bool) ($_SERVER['APP_DEBUG'] ?? ('prod' !== $env)) && !$input->hasParameterOption('--no-debug', true); 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 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "project", 3 | "license": "proprietary", 4 | "require": { 5 | "php": "^7.1.3", 6 | "ext-iconv": "*", 7 | "doctrine/doctrine-bundle": "^1.8", 8 | "doctrine/doctrine-migrations-bundle": "^1.3", 9 | "friendsofsymfony/http-cache-bundle": "^2.2", 10 | "friendsofsymfony/rest-bundle": "^2.3", 11 | "guzzlehttp/psr7": "^1.4", 12 | "jms/serializer-bundle": "^2.3", 13 | "lexik/jwt-authentication-bundle": "^2.4", 14 | "nelmio/api-doc-bundle": "^3.2", 15 | "nelmio/cors-bundle": "^1.5", 16 | "php-http/guzzle6-adapter": "^1.1", 17 | "predis/predis": "^1.1", 18 | "sensio/framework-extra-bundle": "^5.1", 19 | "sensiolabs/security-checker": "^4.1", 20 | "snc/redis-bundle": "^2.1", 21 | "symfony/asset": "^4.0", 22 | "symfony/console": "^4.0", 23 | "symfony/expression-language": "^4.0", 24 | "symfony/flex": "^1.0", 25 | "symfony/framework-bundle": "^4.0", 26 | "symfony/lts": "^4@dev", 27 | "symfony/maker-bundle": "^1.3", 28 | "symfony/options-resolver": "^4.0", 29 | "symfony/orm-pack": "^1.0", 30 | "symfony/polyfill-apcu": "^1.7", 31 | "symfony/security-bundle": "^4.0", 32 | "symfony/swiftmailer-bundle": "^3.2", 33 | "symfony/translation": "^4.0", 34 | "symfony/twig-bundle": "^4.0", 35 | "symfony/validator": "^4.0", 36 | "symfony/yaml": "^4.0", 37 | "twig/extensions": "^1.5", 38 | "willdurand/hateoas-bundle": "^1.4" 39 | }, 40 | "require-dev": { 41 | "doctrine/doctrine-fixtures-bundle": "^3.0", 42 | "nelmio/alice": "^3.3", 43 | "symfony/debug-bundle": "^4.0", 44 | "symfony/dotenv": "^4.0", 45 | "symfony/web-profiler-bundle": "^4.0", 46 | "symfony/web-server-bundle": "^4.0" 47 | }, 48 | "config": { 49 | "preferred-install": { 50 | "*": "dist" 51 | }, 52 | "sort-packages": true 53 | }, 54 | "autoload": { 55 | "psr-4": { 56 | "App\\": "src/" 57 | } 58 | }, 59 | "autoload-dev": { 60 | "psr-4": { 61 | "App\\Tests\\": "tests/" 62 | } 63 | }, 64 | "replace": { 65 | "symfony/polyfill-iconv": "*", 66 | "symfony/polyfill-php71": "*", 67 | "symfony/polyfill-php70": "*", 68 | "symfony/polyfill-php56": "*" 69 | }, 70 | "scripts": { 71 | "auto-scripts": { 72 | "cache:clear": "symfony-cmd", 73 | "assets:install --symlink --relative %PUBLIC_DIR%": "symfony-cmd", 74 | "security-checker security:check": "script" 75 | }, 76 | "post-install-cmd": [ 77 | "@auto-scripts" 78 | ], 79 | "post-update-cmd": [ 80 | "@auto-scripts" 81 | ], 82 | "reset-db": [ 83 | "php bin/console doctrine:schema:drop --force; php bin/console doctrine:schema:create; php bin/console doctrine:fixtures:load -q;" 84 | ] 85 | }, 86 | "conflict": { 87 | "symfony/symfony": "*" 88 | }, 89 | "extra": { 90 | "symfony": { 91 | "id": "01C9TPWG8AFN6AVVY6H4M8TR6P", 92 | "allow-contrib": false 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Symfony\Bundle\WebServerBundle\WebServerBundle::class => ['dev' => true], 6 | Doctrine\Bundle\DoctrineCacheBundle\DoctrineCacheBundle::class => ['all' => true], 7 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 8 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 9 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], 10 | Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], 11 | Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], 12 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 13 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], 14 | Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['all' => true], 15 | Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true], 16 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], 17 | FOS\RestBundle\FOSRestBundle::class => ['all' => true], 18 | JMS\SerializerBundle\JMSSerializerBundle::class => ['all' => true], 19 | Bazinga\Bundle\HateoasBundle\BazingaHateoasBundle::class => ['all' => true], 20 | Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true], 21 | Snc\RedisBundle\SncRedisBundle::class => ['all' => true], 22 | Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true], 23 | FOS\HttpCacheBundle\FOSHttpCacheBundle::class => ['all' => true], 24 | Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], 25 | ]; 26 | -------------------------------------------------------------------------------- /config/packages/dev/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: true 4 | -------------------------------------------------------------------------------- /config/packages/dev/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/dev/swiftmailer.yaml: -------------------------------------------------------------------------------- 1 | # See https://symfony.com/doc/current/email/dev_environment.html 2 | swiftmailer: 3 | # send all emails to a specific address 4 | #delivery_addresses: ['me@example.com'] 5 | -------------------------------------------------------------------------------- /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_pgsql' 12 | charset: UTF8 13 | 14 | # With Symfony 3.3, remove the `resolve:` prefix 15 | url: '%env(resolve:DATABASE_URL)%' 16 | orm: 17 | auto_generate_proxy_classes: '%kernel.debug%' 18 | naming_strategy: doctrine.orm.naming_strategy.underscore 19 | auto_mapping: true 20 | mappings: 21 | App: 22 | is_bundle: false 23 | type: annotation 24 | dir: '%kernel.project_dir%/src/Entity' 25 | prefix: 'App\Entity' 26 | alias: App 27 | -------------------------------------------------------------------------------- /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/fos_http_cache.yaml: -------------------------------------------------------------------------------- 1 | fos_http_cache: 2 | cache_control: 3 | defaults: 4 | overwrite: false 5 | rules: 6 | # match everything to set defaults 7 | - 8 | match: 9 | path: ^/ 10 | headers: 11 | cache_control: { public: true, max_age: 60, s_maxage: 60 } 12 | vary: [X-Accept-Version, Accept-Encoding] 13 | etag: true -------------------------------------------------------------------------------- /config/packages/fos_rest.yaml: -------------------------------------------------------------------------------- 1 | # Read the documentation: https://symfony.com/doc/master/bundles/FOSRestBundle/index.html 2 | fos_rest: 3 | routing_loader: 4 | default_format: json 5 | include_format: false 6 | param_fetcher_listener: true 7 | view: 8 | view_response_listener: true 9 | body_converter: 10 | enabled: true 11 | validate: true 12 | validation_errors_argument: validationErrors 13 | exception: 14 | enabled: true 15 | exception_controller: 'App\Controller\ExceptionController::showAction' 16 | serializer: 17 | groups: ['Default'] 18 | versioning: 19 | enabled: true 20 | default_version: v1 21 | resolvers: 22 | query: 23 | enabled: true 24 | parameter_name: version 25 | format_listener: 26 | enabled: true 27 | rules: 28 | - { path: ^/, prefer_extension: true, fallback_format: json, priorities: [ json ] } 29 | # exception: 30 | # codes: 31 | # App\Exception\MyException: 403 32 | # messages: 33 | # App\Exception\MyException: Forbidden area. 34 | # format_listener: 35 | # rules: 36 | # - { path: ^/api, prefer_extension: true, fallback_format: json, priorities: [ json, html ] } 37 | -------------------------------------------------------------------------------- /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/framework_extra.yaml: -------------------------------------------------------------------------------- 1 | sensio_framework_extra: 2 | router: { annotations: true } 3 | request: { converters: true, auto_convert: true } -------------------------------------------------------------------------------- /config/packages/lexik_jwt_authentication.yaml: -------------------------------------------------------------------------------- 1 | lexik_jwt_authentication: 2 | private_key_path: '%kernel.project_dir%/%env(JWT_PRIVATE_KEY_PATH)%' 3 | public_key_path: '%kernel.project_dir%/%env(JWT_PUBLIC_KEY_PATH)%' 4 | pass_phrase: '%env(JWT_PASSPHRASE)%' 5 | -------------------------------------------------------------------------------- /config/packages/nelmio_api_doc.yaml: -------------------------------------------------------------------------------- 1 | nelmio_api_doc: 2 | documentation: 3 | info: 4 | title: "Movies API" 5 | description: "Movies database API" 6 | version: 1.0.0 7 | routes: 8 | path_patterns: 9 | - ^/api(?!/doc$) 10 | models: 11 | use_jms: true -------------------------------------------------------------------------------- /config/packages/nelmio_cors.yaml: -------------------------------------------------------------------------------- 1 | nelmio_cors: 2 | defaults: 3 | allow_credentials: false 4 | allow_origin: [] 5 | allow_headers: [] 6 | allow_methods: [] 7 | expose_headers: [] 8 | max_age: 0 9 | hosts: [] 10 | origin_regex: false 11 | forced_allow_origin_value: ~ 12 | paths: 13 | '^/': 14 | allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] 15 | allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'] 16 | allow_headers: ['Content-Type', 'Authorization'] 17 | max_age: 3600 -------------------------------------------------------------------------------- /config/packages/prod/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | orm: 3 | metadata_cache_driver: 4 | type: service 5 | id: doctrine.system_cache_provider 6 | query_cache_driver: 7 | type: service 8 | id: doctrine.system_cache_provider 9 | result_cache_driver: 10 | type: service 11 | id: doctrine.result_cache_provider 12 | 13 | services: 14 | doctrine.result_cache_provider: 15 | class: Symfony\Component\Cache\DoctrineProvider 16 | public: false 17 | arguments: 18 | - '@doctrine.result_cache_pool' 19 | doctrine.system_cache_provider: 20 | class: Symfony\Component\Cache\DoctrineProvider 21 | public: false 22 | arguments: 23 | - '@doctrine.system_cache_pool' 24 | 25 | framework: 26 | cache: 27 | pools: 28 | doctrine.result_cache_pool: 29 | adapter: cache.app 30 | doctrine.system_cache_pool: 31 | adapter: cache.system 32 | -------------------------------------------------------------------------------- /config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: ~ 4 | -------------------------------------------------------------------------------- /config/packages/security.yaml: -------------------------------------------------------------------------------- 1 | security: 2 | encoders: 3 | App\Entity\User: 4 | algorithm: bcrypt 5 | # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers 6 | providers: 7 | database: 8 | entity: 9 | class: App:User 10 | property: username 11 | firewalls: 12 | dev: 13 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 14 | security: false 15 | secured_api: 16 | anonymous: true 17 | stateless: true 18 | guard: 19 | authenticators: 20 | - App\Security\TokenAuthenticator 21 | 22 | # Easy way to control access for large sections of your site 23 | # Note: Only the *first* access control that matches will be used 24 | access_control: 25 | # - { path: ^/admin, roles: ROLE_ADMIN } 26 | # - { path: ^/profile, roles: ROLE_USER } 27 | -------------------------------------------------------------------------------- /config/packages/snc_redis.yaml: -------------------------------------------------------------------------------- 1 | snc_redis: 2 | clients: 3 | default: 4 | type: predis 5 | alias: default 6 | dsn: redis://localhost -------------------------------------------------------------------------------- /config/packages/swiftmailer.yaml: -------------------------------------------------------------------------------- 1 | swiftmailer: 2 | url: '%env(MAILER_URL)%' 3 | spool: { type: 'memory' } 4 | -------------------------------------------------------------------------------- /config/packages/test/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | test: true 3 | session: 4 | storage_id: session.storage.mock_file 5 | -------------------------------------------------------------------------------- /config/packages/test/swiftmailer.yaml: -------------------------------------------------------------------------------- 1 | swiftmailer: 2 | disable_delivery: true 3 | -------------------------------------------------------------------------------- /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 | paths: 5 | - '%kernel.project_dir%/translations' 6 | fallbacks: 7 | - '%locale%' 8 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | paths: ['%kernel.project_dir%/templates'] 3 | debug: '%kernel.debug%' 4 | strict_variables: '%kernel.debug%' 5 | -------------------------------------------------------------------------------- /config/packages/twig_extensions.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | public: false 4 | autowire: true 5 | autoconfigure: true 6 | 7 | #Twig\Extensions\ArrayExtension: ~ 8 | #Twig\Extensions\DateExtension: ~ 9 | #Twig\Extensions\IntlExtension: ~ 10 | #Twig\Extensions\TextExtension: ~ 11 | -------------------------------------------------------------------------------- /config/routes.yaml: -------------------------------------------------------------------------------- 1 | #index: 2 | # path: / 3 | # controller: App\Controller\DefaultController::index 4 | api: 5 | prefix: /api 6 | resource: '../src/Controller' 7 | 8 | movies: 9 | type: rest 10 | resource: App\Controller\MoviesController 11 | 12 | persons: 13 | type: rest 14 | resource: App\Controller\PersonsController 15 | 16 | user: 17 | resource: App\Controller\UsersController 18 | type: rest 19 | 20 | tokens: 21 | resource: App\Controller\TokensController 22 | type: rest 23 | 24 | images: 25 | resource: App\Controller\ImagesController 26 | type: rest 27 | 28 | app.swagger_ui: 29 | path: /api/doc 30 | methods: GET 31 | defaults: { _controller: nelmio_api_doc.controller.swagger_ui } -------------------------------------------------------------------------------- /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/services.yaml: -------------------------------------------------------------------------------- 1 | # Put parameters here that don't need to change on each machine where the app is deployed 2 | # https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration 3 | parameters: 4 | locale: 'en' 5 | 6 | services: 7 | # default configuration for services in *this* file 8 | _defaults: 9 | autowire: true # Automatically injects dependencies in your services. 10 | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. 11 | public: false # Allows optimizing the container by removing unused services; this also means 12 | # fetching services directly from the container via $container->get() won't work. 13 | # The best practice is to be explicit about your dependencies anyway. 14 | 15 | # makes classes in src/ available to be used as services 16 | # this creates a service per class whose id is the fully-qualified class name 17 | App\: 18 | resource: '../src/*' 19 | exclude: '../src/{Entity,Migrations,Tests,Kernel.php}' 20 | 21 | # controllers are imported separately to make sure services can be injected 22 | # as action arguments even if you don't extend any base controller class 23 | App\Controller\: 24 | resource: '../src/Controller' 25 | tags: ['controller.service_arguments'] 26 | 27 | # add more service definitions when explicit configuration is needed 28 | # please note that last definitions always *replace* previous ones 29 | App\Serializer\DoctrineEntityDeserializationSubscriber: 30 | tags: ['jms_serializer.event_subscriber'] 31 | public: true 32 | 33 | Predis\Client: 34 | autowire: true 35 | 36 | App\Entity\EntityMerger: 37 | autowire: true 38 | 39 | App\Controller\ImagesController: 40 | arguments: 41 | $imageDirectory: '%kernel.project_dir%/public/uploads/images' 42 | $imageBaseUrl: '/uploads/images/' -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | load(__DIR__.'/../.env'); 17 | } 18 | 19 | $env = $_SERVER['APP_ENV'] ?? 'dev'; 20 | $debug = (bool) ($_SERVER['APP_DEBUG'] ?? ('prod' !== $env)); 21 | 22 | if ($debug) { 23 | umask(0000); 24 | 25 | Debug::enable(); 26 | } 27 | 28 | if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? false) { 29 | Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST); 30 | } 31 | 32 | if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? false) { 33 | Request::setTrustedHosts(explode(',', $trustedHosts)); 34 | } 35 | 36 | $kernel = new Kernel($env, $debug); 37 | $kernel = new CacheKernel($kernel); 38 | Request::enableHttpMethodParameterOverride(); 39 | $request = Request::createFromGlobals(); 40 | $response = $kernel->handle($request); 41 | $response->send(); 42 | $kernel->terminate($request, $response); 43 | -------------------------------------------------------------------------------- /public/uploads/images/ecb66f083f14b45fe68dbf07caef9c5b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neverovski/symfony-restful-api/4510c4a9f5d2ce6a1bb7554f8d976c98c6a37239/public/uploads/images/ecb66f083f14b45fe68dbf07caef9c5b.png -------------------------------------------------------------------------------- /src/Annotation/DeserializeEntity.php: -------------------------------------------------------------------------------- 1 | addSubscriber(new \FOS\HttpCache\SymfonyCache\CustomTtlListener()); 21 | $this->addSubscriber(new \FOS\HttpCache\SymfonyCache\PurgeListener()); 22 | $this->addSubscriber(new \FOS\HttpCache\SymfonyCache\RefreshListener()); 23 | $this->addSubscriber(new \FOS\HttpCache\SymfonyCache\UserContextListener()); 24 | 25 | if (isset($options['debug']) && $options['debug']) { 26 | $this->addSubscriber(new \FOS\HttpCache\SymfonyCache\DebugListener()); 27 | } 28 | } 29 | 30 | /** 31 | * @param \Symfony\Component\HttpFoundation\Request $request 32 | * @param bool $catch 33 | * @return \Symfony\Component\HttpFoundation\Response 34 | */ 35 | public function fetch(\Symfony\Component\HttpFoundation\Request $request, $catch = false) 36 | { 37 | return parent::fetch($request, $catch); 38 | } 39 | 40 | /** 41 | * @param \Symfony\Component\HttpFoundation\Request $request 42 | * @param bool $catch 43 | * @return \Symfony\Component\HttpFoundation\Response 44 | */ 45 | protected function invalidate(\Symfony\Component\HttpFoundation\Request $request, $catch = false) 46 | { 47 | if ('PURGE' !== $request->getMethod()) { 48 | return parent::invalidate($request, $catch); 49 | } 50 | 51 | $response = new \Symfony\Component\HttpFoundation\Response(); 52 | 53 | if ($this->getStore()->purge($request->getUri())) { 54 | $response->setStatusCode(200, 'Purged'); 55 | } else { 56 | $response->setStatusCode(404, 'Not found'); 57 | } 58 | 59 | return $response; 60 | } 61 | } -------------------------------------------------------------------------------- /src/Controller/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neverovski/symfony-restful-api/4510c4a9f5d2ce6a1bb7554f8d976c98c6a37239/src/Controller/.gitignore -------------------------------------------------------------------------------- /src/Controller/ExceptionController.php: -------------------------------------------------------------------------------- 1 | getView($exception->getStatusCode(), json_decode($exception->getMessage(), true)); 27 | } 28 | 29 | if ($exception instanceof HttpException) { 30 | return $this->getView($exception->getStatusCode(), $exception->getMessage()); 31 | } 32 | 33 | return $this->getView(null, 'Unexpected error occured'); 34 | } 35 | 36 | /** 37 | * @param int|null $statusCode 38 | * @param $message 39 | * @return View 40 | */ 41 | private function getView(?int $statusCode, $message): View 42 | { 43 | $data = [ 44 | 'code' => $statusCode ?? 500, 45 | 'message' => $message 46 | ]; 47 | 48 | return $this->view($data, $statusCode ?? 500); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Controller/ImagesController.php: -------------------------------------------------------------------------------- 1 | imageRepository = $imageRepository; 54 | $this->imageDirectory = $imageDirectory; 55 | $this->imageBaseUrl = $imageBaseUrl; 56 | } 57 | 58 | /** 59 | * @Rest\View() 60 | * @Rest\Get("/images", name="get_images") 61 | * @SWG\Get( 62 | * tags={"Image"}, 63 | * summary="Gets the all image", 64 | * consumes={"application/json"}, 65 | * produces={"application/json"}, 66 | * @SWG\Response(response="200", description="Returned when successful") 67 | * ) 68 | */ 69 | public function getImages() 70 | { 71 | return $this->imageRepository->findAll(); 72 | } 73 | 74 | /** 75 | * @Rest\View(statusCode=201) 76 | * @Rest\Post("/images", name="post_images") 77 | * @ParamConverter( 78 | * "image", 79 | * converter="fos_rest.request_body", 80 | * options={"deserializationContext"={"groups"={"Deserialize"}}} 81 | * ) 82 | * @SWG\Post( 83 | * tags={"Image"}, 84 | * summary="Add a new image resource", 85 | * consumes={"application/json"}, 86 | * produces={"application/json"}, 87 | * @SWG\Response(response="201", description="Returned when resource created"), 88 | * ) 89 | * 90 | * @param Image $image 91 | * @return \FOS\RestBundle\View\View 92 | */ 93 | public function postImages(Image $image) 94 | { 95 | $this->persistImage($image); 96 | 97 | return $this->view($image, Response::HTTP_CREATED)->setHeader( 98 | 'Location', 99 | $this->generateUrl( 100 | 'images_upload_put', 101 | ['image' => $image->getId()] 102 | ) 103 | ); 104 | } 105 | 106 | /** 107 | * @Rest\View() 108 | * @Rest\Put("/images/{image}/upload", name="put_image_upload") 109 | * @SWG\Put( 110 | * tags={"Image"}, 111 | * summary="Edit the image", 112 | * consumes={"application/json"}, 113 | * produces={"application/json"}, 114 | * @SWG\Response(response="200", description="Returned when resource update"), 115 | * @SWG\Response(response="400", description="Returned when invalid date posted") 116 | * ) 117 | * 118 | * @param Image|null $image 119 | * @param Request $request 120 | * @return Response 121 | */ 122 | public function putImageUpload(?Image $image, Request $request) 123 | { 124 | if (null === $image) { 125 | throw new NotFoundHttpException(); 126 | } 127 | 128 | // Read the image content from request body 129 | $content = $request->getContent(); 130 | // Create the temporary upload file (deleted after request finishes) 131 | $tmpFile = tmpfile(); 132 | // Get the temporary file name 133 | $tmpFilePatch = stream_get_meta_data($tmpFile)['uri']; 134 | // Write image content to the temporary file 135 | file_put_contents($tmpFilePatch, $content); 136 | 137 | // Get the file mime-type 138 | $finfo = finfo_open(FILEINFO_MIME_TYPE); 139 | $mimeType = finfo_file($finfo, $tmpFilePatch); 140 | 141 | // Check if it's really an image (never trust client set mime-type!) 142 | if (!in_array($mimeType, ['image/jpeg', 'image/png', 'image/gif'])) { 143 | throw new UnsupportedMediaTypeHttpException( 144 | 'File uploaded is not a valid png/jpeg/gif image' 145 | ); 146 | } 147 | 148 | // Guess the extension based on mime-type 149 | $extensionGuesser = ExtensionGuesser::getInstance(); 150 | // Generate a new random filename 151 | $newFileName = md5(uniqid()) . '.' . $extensionGuesser->guess($mimeType); 152 | 153 | // Copy the temp file to the final uploads directory 154 | copy($tmpFilePatch, $this->imageDirectory . DIRECTORY_SEPARATOR . $newFileName); 155 | 156 | $image->setUrl($this->imageBaseUrl . $newFileName); 157 | 158 | $this->persistImage($image); 159 | 160 | return new Response(null, Response::HTTP_OK); 161 | } 162 | 163 | /** 164 | * @param Image|null $image 165 | */ 166 | public function persistImage(?Image $image): void 167 | { 168 | $manager = $this->getDoctrine()->getManager(); 169 | $manager->persist($image); 170 | $manager->flush(); 171 | } 172 | } -------------------------------------------------------------------------------- /src/Controller/MoviesController.php: -------------------------------------------------------------------------------- 1 | entityMerger = $entityMerger; 62 | $this->moviePagination = $moviePagination; 63 | $this->rolePagination = $rolePagination; 64 | } 65 | 66 | /** 67 | * @Rest\View() 68 | * @Rest\Get("/movies", name="get_movies") 69 | * @SWG\Get( 70 | * tags={"Movie"}, 71 | * summary="Gets the all movie", 72 | * consumes={"application/json"}, 73 | * produces={"application/json"}, 74 | * @SWG\Response(response="200", description="Returned when successful"), 75 | * @SWG\Response(response="404", description="Returned when movie is not found") 76 | * ) 77 | * 78 | * @param Request $request 79 | * @return \Hateoas\Representation\PaginatedRepresentation 80 | */ 81 | public function getMovies(Request $request) 82 | { 83 | $pageRequestFactory = new PageRequestFactory(); 84 | $page = $pageRequestFactory->fromRequest($request); 85 | 86 | $movieFilterDefinitionFactory = new MovieFilterDefinitionFactory(); 87 | $movieFilterDefinition = $movieFilterDefinitionFactory->factory($request); 88 | 89 | return $this->moviePagination->paginate($page, $movieFilterDefinition); 90 | } 91 | 92 | /** 93 | * @Rest\View(statusCode=201) 94 | * @Rest\Post("/movies", name="post_movies") 95 | * @ParamConverter("movie", converter="fos_rest.request_body") * 96 | * @SWG\Post( 97 | * tags={"Movie"}, 98 | * summary="Add a new movie resource", 99 | * consumes={"application/json"}, 100 | * produces={"application/json"}, 101 | * @SWG\Response(response="201", description="Returned when resource created"), 102 | * @SWG\Response(response="400", description="Returned when invalid date posted"), 103 | * @SWG\Response(response="401", description="Returned when not authenticated"), 104 | * @SWG\Response(response="403", description="Returned when token is invalid or expired") 105 | * ) 106 | * 107 | * @param Movie $movie 108 | * @param ConstraintViolationListInterface $validationErrors 109 | * @return Movie 110 | */ 111 | public function postMovies(Movie $movie, ConstraintViolationListInterface $validationErrors) 112 | { 113 | if (count($validationErrors) > 0) { 114 | throw new ValidationException($validationErrors); 115 | } 116 | $manager = $this->getDoctrine()->getManager(); 117 | 118 | $manager->persist($movie); 119 | $manager->flush(); 120 | 121 | return $movie; 122 | } 123 | 124 | /** 125 | * @Rest\View() 126 | * @InvalidateRoute("get_movie", params={"movie" = {"expression" = "movie.getId()"}}) 127 | * @InvalidateRoute("get_movies") 128 | * @Rest\Delete("/movies/{movie}", name="delete_movie") 129 | * @SWG\Delete( 130 | * tags={"Movie"}, 131 | * summary="Delete the movie", 132 | * consumes={"application/json"}, 133 | * produces={"application/json"}, 134 | * @SWG\Response(response="200", description="Returned when successful"), 135 | * @SWG\Response(response="404", description="Returned when movie is not found") 136 | * ) 137 | * 138 | * @param Movie|null $movie 139 | * @return \FOS\RestBundle\View\View 140 | */ 141 | public function deleteMovie(?Movie $movie) 142 | { 143 | if (null === $movie) { 144 | return $this->view(null, 404); 145 | } 146 | 147 | $manager = $this->getDoctrine()->getManager(); 148 | $manager->remove($movie); 149 | $manager->flush(); 150 | } 151 | 152 | /** 153 | * @Rest\View() 154 | * @Rest\Get("/movies/{movie}", name="get_movie") 155 | * @Cache(public=true, maxage=3600, smaxage=3600) 156 | * @SWG\Get( 157 | * tags={"Movie"}, 158 | * summary="Gets the movie", 159 | * consumes={"application/json"}, 160 | * produces={"application/json"}, 161 | * @SWG\Response(response="200", description="Returned when successful"), 162 | * @SWG\Response(response="404", description="Returned when movie is not found") 163 | * ) 164 | * 165 | * @param Movie|null $movie 166 | * @return Movie|\FOS\RestBundle\View\View|null 167 | */ 168 | public function getMovie(?Movie $movie) 169 | { 170 | if (null === $movie) { 171 | return $this->view(null, 404); 172 | } 173 | 174 | return $movie; 175 | } 176 | 177 | /** 178 | * @Rest\View() 179 | * @Rest\Get("/movies/{movie}/roles", name="get_movie_roles") 180 | * 181 | * @param Request $request 182 | * @param Movie $movie 183 | * @return \Hateoas\Representation\PaginatedRepresentation 184 | */ 185 | public function getMovieRoles(Request $request, Movie $movie) 186 | { 187 | $pageRequestFactory = new PageRequestFactory(); 188 | $page = $pageRequestFactory->fromRequest($request); 189 | 190 | $roleFilterDefinitionFactory = new RoleFilterDefinitionFactory(); 191 | $roleFilterDefinition = $roleFilterDefinitionFactory->factory( 192 | $request, 193 | $movie->getId() 194 | ); 195 | 196 | return $this->rolePagination->paginate($page, $roleFilterDefinition); 197 | } 198 | 199 | /** 200 | * @Rest\View(statusCode=201) 201 | * @Rest\Post("/movies/{movie}/roles", name="post_movie_roles") 202 | * @ParamConverter("role", converter="fos_rest.request_body", options={"deserializationContext"={"groups"={"Deserialize"}}}) 203 | * 204 | * @param Movie $movie 205 | * @param Role $role 206 | * @param ConstraintViolationListInterface $validationErrors 207 | * @return Role 208 | */ 209 | public function postMovieRoles(Movie $movie, Role $role, ConstraintViolationListInterface $validationErrors) 210 | { 211 | if (count($validationErrors) > 0) { 212 | throw new ValidationException($validationErrors); 213 | } 214 | 215 | $role->setMovie($movie); 216 | $manager = $this->getDoctrine()->getManager(); 217 | 218 | $manager->persist($role); 219 | $movie->getRoles()->add($role); 220 | 221 | $manager->persist($movie); 222 | $manager->flush(); 223 | 224 | return $role; 225 | } 226 | 227 | /** 228 | * @Rest\View() 229 | * @Rest\Put("/movies/{movie}", name="put_movie") 230 | * @ParamConverter("modifiedMovie", converter="fos_rest.request_body", 231 | * options={"validator" = {"groups" = {"Patch"}}} 232 | * ) 233 | * @Security("is_authenticated()") 234 | * @SWG\Put( 235 | * tags={"Movie"}, 236 | * summary="Edit the movie", 237 | * consumes={"application/json"}, 238 | * produces={"application/json"}, 239 | * @SWG\Response(response="201", description="Returned when resource update"), 240 | * @SWG\Response(response="400", description="Returned when invalid date posted"), 241 | * @SWG\Response(response="401", description="Returned when not authenticated"), 242 | * @SWG\Response(response="403", description="Returned when token is invalid or expired") 243 | * ) 244 | * 245 | * @param Movie|null $movie 246 | * @param Movie $modifiedMovie 247 | * @param ConstraintViolationListInterface $validationErrors 248 | * @return Movie|\FOS\RestBundle\View\View|null 249 | */ 250 | public function putMovie(?Movie $movie, Movie $modifiedMovie, ConstraintViolationListInterface $validationErrors) 251 | { 252 | if (null === $movie) { 253 | return $this->view(null, 404); 254 | } 255 | 256 | if (count($validationErrors) > 0) { 257 | throw new ValidationException($validationErrors); 258 | } 259 | 260 | $this->entityMerger->merge($movie, $modifiedMovie); 261 | 262 | $manager = $this->getDoctrine()->getManager(); 263 | $manager->persist($movie); 264 | $manager->flush(); 265 | 266 | return $movie; 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/Controller/PersonsController.php: -------------------------------------------------------------------------------- 1 | personPagination = $personPagination; 40 | } 41 | 42 | /** 43 | * @Rest\View() 44 | * @Rest\Get("/persons", name="get_persons") 45 | * @SWG\Get( 46 | * tags={"Person"}, 47 | * summary="Gets the all person", 48 | * consumes={"application/json"}, 49 | * produces={"application/json"}, 50 | * @SWG\Response(response="200", description="Returned when successful"), 51 | * @SWG\Response(response="404", description="Returned when movie is not found") 52 | * ) 53 | * 54 | * @param Request $request 55 | * @return \Hateoas\Representation\PaginatedRepresentation 56 | */ 57 | public function getPersons(Request $request) 58 | { 59 | $pageRequestFactory = new PageRequestFactory(); 60 | $page = $pageRequestFactory->fromRequest($request); 61 | 62 | $personFilterDefinitionFactory = new PersonFilterDefinitionFactory(); 63 | $personFilterDefinition = $personFilterDefinitionFactory->factory($request); 64 | 65 | return $this->personPagination->paginate($page, $personFilterDefinition); 66 | } 67 | 68 | /** 69 | * @Rest\View(statusCode=201) 70 | * @Rest\Post("/persons", name="post_persons") 71 | * @ParamConverter("person", converter="fos_rest.request_body") 72 | * @SWG\Post( 73 | * tags={"Person"}, 74 | * summary="Add a new person resource", 75 | * consumes={"application/json"}, 76 | * produces={"application/json"}, 77 | * @SWG\Response(response="200", description="Returned when successful"), 78 | * @SWG\Response(response="404", description="Returned when movie is not found") 79 | * ) 80 | * 81 | * @param Person $person 82 | * @param ConstraintViolationListInterface $validationErrors 83 | * @return Person 84 | */ 85 | public function postPersons(Person $person, ConstraintViolationListInterface $validationErrors) 86 | { 87 | if (count($validationErrors) > 0) { 88 | throw new ValidationException($validationErrors); 89 | } 90 | $manager = $this->getDoctrine()->getManager(); 91 | 92 | $manager->persist($person); 93 | $manager->flush(); 94 | 95 | return $person; 96 | } 97 | 98 | /** 99 | * @Rest\View() 100 | * @Rest\Delete("/persons/{person}", name="delete_person") 101 | * @SWG\Delete( 102 | * tags={"Person"}, 103 | * summary="Delete the person", 104 | * consumes={"application/json"}, 105 | * produces={"application/json"}, 106 | * @SWG\Response(response="200", description="Returned when successful"), 107 | * @SWG\Response(response="404", description="Returned when movie is not found") 108 | * ) 109 | * 110 | * @param Person|null $person 111 | * @return \FOS\RestBundle\View\View 112 | */ 113 | public function deletePerson(?Person $person) 114 | { 115 | if (null === $person) { 116 | return $this->view(null, 404); 117 | } 118 | 119 | $manager = $this->getDoctrine()->getManager(); 120 | $manager->remove($person); 121 | $manager->flush(); 122 | } 123 | 124 | /** 125 | * @Rest\View() 126 | * @Rest\Get("/persons/{person}", name="get_person") 127 | * @SWG\Get( 128 | * tags={"Person"}, 129 | * summary="Gets the person", 130 | * consumes={"application/json"}, 131 | * produces={"application/json"}, 132 | * @SWG\Response(response="200", description="Returned when successful"), 133 | * @SWG\Response(response="404", description="Returned when movie is not found") 134 | * ) 135 | * 136 | * @param Person|null $person 137 | * @return Person|\FOS\RestBundle\View\View|null 138 | */ 139 | public function getPeron(?Person $person) 140 | { 141 | if (null === $person) { 142 | return $this->view(null, 404); 143 | } 144 | 145 | return $person; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Controller/TokensController.php: -------------------------------------------------------------------------------- 1 | passwordEncoder = $passwordEncoder; 53 | $this->jwtEncoder = $jwtEncoder; 54 | $this->tokenStorage = $tokenStorage; 55 | } 56 | 57 | /** 58 | * @Rest\View(statusCode=201) 59 | * @Rest\Post("/tokens", name="post_token") 60 | * @SWG\Post( 61 | * tags={"User"}, 62 | * summary="Add a new token", 63 | * consumes={"application/json"}, 64 | * produces={"application/json"}, 65 | * @SWG\Response(response="200", description="Returned when successful"), 66 | * @SWG\Response(response="404", description="Returned when movie is not found") 67 | * ) 68 | * 69 | * @param Request $request 70 | * @return JsonResponse 71 | * @throws \Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTEncodeFailureException 72 | */ 73 | public function postToken(Request $request) 74 | { 75 | $user = $this->getDoctrine()->getRepository('App:User')->findOneBy(['username' => $request->getUser()]); 76 | if (!$user) { 77 | throw new BadCredentialsException(); 78 | } 79 | 80 | $isPasswordValid = $this->passwordEncoder->isPasswordValid($user, $request->getPassword()); 81 | 82 | if (!$isPasswordValid) { 83 | throw new BadCredentialsException(); 84 | } 85 | 86 | $token = $this->jwtEncoder->encode( 87 | [ 88 | 'username' => $user->getUsername(), 89 | 'exp' => time() + 3600 90 | ] 91 | ); 92 | 93 | $this->tokenStorage->isTokenValid( 94 | $user->getUsername(), 95 | $token 96 | ); 97 | 98 | return new JsonResponse(['token' => $token]); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Controller/UsersController.php: -------------------------------------------------------------------------------- 1 | passwordEncoder = $passwordEncoder; 60 | $this->jwtEncoder = $jwtEncoder; 61 | $this->tokenStorage = $tokenStorage; 62 | $this->entityMerger = $entityMerger; 63 | } 64 | 65 | /** 66 | * @Rest\View() 67 | * @Rest\Get("/users/{theUser}", name="get_user") 68 | * @Security("is_granted('show', theUser)", message="Access denied") 69 | * @SWG\Get( 70 | * tags={"User"}, 71 | * summary="Get the user", 72 | * consumes={"application/json"}, 73 | * produces={"application/json"}, 74 | * @SWG\Response(response="200", description="Returned when successful"), 75 | * @SWG\Response(response="404", description="Returned when movie is not found") 76 | * ) 77 | * 78 | * @param User|null $theUser 79 | * @return User|null 80 | */ 81 | public function getUsers(?User $theUser) 82 | { 83 | if (null === $theUser) { 84 | throw new NotFoundHttpException(); 85 | } 86 | 87 | return $theUser; 88 | } 89 | 90 | /** 91 | * @Rest\View(statusCode=201) 92 | * @Rest\Post("/users", name="post_user") 93 | * @ParamConverter( 94 | * "user", 95 | * converter="fos_rest.request_body", 96 | * options={"deserializationContext"={"groups"={"Deserialize"}}} 97 | * ) 98 | * @SWG\Post( 99 | * tags={"User"}, 100 | * summary="Add a new user resource", 101 | * consumes={"application/json"}, 102 | * produces={"application/json"}, 103 | * @SWG\Response(response="200", description="Returned when successful"), 104 | * @SWG\Response(response="404", description="Returned when movie is not found") 105 | * ) 106 | * 107 | * @param User $user 108 | * @param ConstraintViolationListInterface $validationErrors 109 | * @return User 110 | */ 111 | public function postUser(User $user, ConstraintViolationListInterface $validationErrors) 112 | { 113 | if (count($validationErrors) > 0) { 114 | throw new ValidationException($validationErrors); 115 | } 116 | 117 | $this->encodePassword($user); 118 | $user->setRoles([User::ROLE_USER]); 119 | $this->persistUser($user); 120 | 121 | return $user; 122 | } 123 | 124 | /** 125 | * @Rest\View() 126 | * @Rest\Put("/users/{theUser}", name="put_user") 127 | * @ParamConverter( 128 | * "modifiedUser", 129 | * converter="fos_rest.request_body", 130 | * options={ 131 | * "validator"={"groups"={"Patch"}}, 132 | * "deserializationContext"={"groups"={"Deserialize"}} 133 | * } 134 | * ) 135 | * @Security("is_granted('edit', theUser)", message="Access denied") 136 | * @SWG\Put( 137 | * tags={"User"}, 138 | * summary="Edit the user", 139 | * consumes={"application/json"}, 140 | * produces={"application/json"}, 141 | * @SWG\Response(response="200", description="Returned when successful"), 142 | * @SWG\Response(response="404", description="Returned when movie is not found") 143 | * ) 144 | * 145 | * @param User|null $theUser 146 | * @param User $modifiedUser 147 | * @param ConstraintViolationListInterface $validationErrors 148 | * @return User|null 149 | */ 150 | public function putUser(?User $theUser, User $modifiedUser, ConstraintViolationListInterface $validationErrors) 151 | { 152 | if (null === $theUser) { 153 | throw new NotFoundHttpException(); 154 | } 155 | 156 | if (count($validationErrors) > 0) { 157 | throw new ValidationException($validationErrors); 158 | } 159 | 160 | if (empty($modifiedUser->getPassword())) { 161 | $modifiedUser->setPassword(null); 162 | } 163 | $this->entityMerger->merge($theUser, $modifiedUser); 164 | 165 | $this->encodePassword($theUser); 166 | $this->persistUser($theUser); 167 | 168 | if ($modifiedUser->getPassword()) { 169 | $this->tokenStorage->invalidateToken($theUser->getUsername()); 170 | } 171 | 172 | return $theUser; 173 | } 174 | 175 | /** 176 | * @param User $user 177 | */ 178 | protected function encodePassword(User $user): void 179 | { 180 | $user->setPassword( 181 | $this->passwordEncoder->encodePassword( 182 | $user, 183 | $user->getPassword() 184 | ) 185 | ); 186 | } 187 | 188 | /** 189 | * @param User $user 190 | */ 191 | protected function persistUser(User $user): void 192 | { 193 | $manager = $this->getDoctrine()->getManager(); 194 | $manager->persist($user); 195 | $manager->flush(); 196 | } 197 | } -------------------------------------------------------------------------------- /src/DataFixtures/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neverovski/symfony-restful-api/4510c4a9f5d2ce6a1bb7554f8d976c98c6a37239/src/DataFixtures/.gitignore -------------------------------------------------------------------------------- /src/DataFixtures/Fixtures/movie.yaml: -------------------------------------------------------------------------------- 1 | App\Entity\Movie: 2 | movie_{1..100}: 3 | title: 4 | year: 5 | time: 6 | description: 7 | -------------------------------------------------------------------------------- /src/DataFixtures/Fixtures/person.yaml: -------------------------------------------------------------------------------- 1 | App\Entity\Person: 2 | person_{1..1000}: 3 | firstname: 4 | lastname: 5 | dateOfBirth: -------------------------------------------------------------------------------- /src/DataFixtures/Fixtures/role.yaml: -------------------------------------------------------------------------------- 1 | App\Entity\Person: 2 | person_{1..1000}: 3 | firstname: 4 | lastname: 5 | dateOfBirth: 6 | 7 | App\Entity\Movie: 8 | movie_{1..100}: 9 | title: 10 | year: 11 | time: 12 | description: 13 | 14 | App\Entity\Role: 15 | role_{1..5000}: 16 | person: '@person_' 17 | movie: '@movie_' 18 | playedName: -------------------------------------------------------------------------------- /src/DataFixtures/LoadMovieData.php: -------------------------------------------------------------------------------- 1 | loadFile(__DIR__ . '/Fixtures/movie.yaml')->getObjects(); 15 | foreach($objectSet as $object) { 16 | $manager->persist($object); 17 | $manager->flush(); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/DataFixtures/LoadPersonData.php: -------------------------------------------------------------------------------- 1 | loadFile(__DIR__ . '/Fixtures/person.yaml')->getObjects(); 15 | foreach($objectSet as $object) { 16 | $manager->persist($object); 17 | $manager->flush(); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/DataFixtures/LoadRoleData.php: -------------------------------------------------------------------------------- 1 | loadFile(__DIR__ . '/Fixtures/role.yaml')->getObjects(); 21 | foreach($objectSet as $object) { 22 | $manager->persist($object); 23 | $manager->flush(); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/DataFixtures/LoadUserData.php: -------------------------------------------------------------------------------- 1 | encoder = $encoder; 23 | } 24 | 25 | public function load(ObjectManager $manager) 26 | { 27 | $user = new User(); 28 | $user->setUsername('neverovski'); 29 | $password = $this->encoder->encodePassword($user, 'Security123!'); 30 | $user->setRoles([User::ROLE_ADMIN]); 31 | $user->setPassword($password); 32 | 33 | $manager->persist($user); 34 | $manager->flush(); 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/Entity/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neverovski/symfony-restful-api/4510c4a9f5d2ce6a1bb7554f8d976c98c6a37239/src/Entity/.gitignore -------------------------------------------------------------------------------- /src/Entity/EntityMerger.php: -------------------------------------------------------------------------------- 1 | reader = $reader; 20 | } 21 | 22 | /** 23 | * @param $entity 24 | * @param @changes 25 | */ 26 | public function merge($entity, $changes): void 27 | { 28 | // Get $entity class name or false if it's not a class 29 | $entityClassName = get_class($entity); 30 | 31 | if (false === $entityClassName) { 32 | throw new \InvalidArgumentException('$entity is not a class'); 33 | } 34 | 35 | // Get $changes class name or false if it's not a class 36 | $changesClassName = get_class($changes); 37 | 38 | if (false === $changesClassName) { 39 | throw new \InvalidArgumentException('$changes is not a class'); 40 | } 41 | 42 | // If $changes object is of the same class as $entity 43 | if (!is_a($changes, $entityClassName)) { 44 | throw new \InvalidArgumentException('Cannot merge object of class $changesClassName with object of class $entityClassName'); 45 | } 46 | 47 | $entityReflection = new \ReflectionObject($entity); 48 | $changesReflection = new \ReflectionObject($changes); 49 | 50 | foreach ($changesReflection->getProperties() as $changedProperty) { 51 | $changedProperty->setAccessible(true); 52 | $changedPropertyValue = $changedProperty->getValue($changes); 53 | 54 | // Ignore $changes property with null value 55 | if (null === $changedPropertyValue) { 56 | continue; 57 | } 58 | 59 | // Ignore $changes property if it's not present on $entity 60 | if (!$entityReflection->hasProperty($changedProperty->getName())) { 61 | continue; 62 | } 63 | 64 | $entityProperty = $entityReflection->getProperty($changedProperty->getName()); 65 | $annotation = $this->reader->getPropertyAnnotation($entityProperty, Id::class); 66 | 67 | // Ignore $changes property that has Doctrine @Id annotation 68 | if (null !== $annotation) { 69 | continue; 70 | } 71 | 72 | $entityProperty->setAccessible(true); 73 | $entityProperty->setValue($entity, $changedPropertyValue); 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /src/Entity/Image.php: -------------------------------------------------------------------------------- 1 | id; 42 | } 43 | 44 | public function getDescription(): ?string 45 | { 46 | return $this->description; 47 | } 48 | 49 | public function setDescription(?string $description): self 50 | { 51 | $this->description = $description; 52 | 53 | return $this; 54 | } 55 | 56 | public function getUrl(): ?string 57 | { 58 | return $this->url; 59 | } 60 | 61 | public function setUrl(?string $url): self 62 | { 63 | $this->url = $url; 64 | 65 | return $this; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Entity/Movie.php: -------------------------------------------------------------------------------- 1 | roles = new ArrayCollection(); 70 | } 71 | 72 | /** 73 | * @return int $id 74 | */ 75 | public function getId() 76 | { 77 | return $this->id; 78 | } 79 | 80 | /** 81 | * @return null|string 82 | */ 83 | public function getTitle(): ?string 84 | { 85 | return $this->title; 86 | } 87 | 88 | /** 89 | * @param string $title 90 | * @return Movie 91 | */ 92 | public function setTitle(string $title): self 93 | { 94 | $this->title = $title; 95 | 96 | return $this; 97 | } 98 | 99 | /** 100 | * @return int|null 101 | */ 102 | public function getYear(): ?int 103 | { 104 | return $this->year; 105 | } 106 | 107 | /** 108 | * @param int $year 109 | * @return Movie 110 | */ 111 | public function setYear(int $year): self 112 | { 113 | $this->year = $year; 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * @return int|null 120 | */ 121 | public function getTime(): ?int 122 | { 123 | return $this->time; 124 | } 125 | 126 | /** 127 | * @param int $time 128 | * @return Movie 129 | */ 130 | public function setTime(int $time): self 131 | { 132 | $this->time = $time; 133 | 134 | return $this; 135 | } 136 | 137 | /** 138 | * @return null|string 139 | */ 140 | public function getDescription(): ?string 141 | { 142 | return $this->description; 143 | } 144 | 145 | /** 146 | * @param null|string $description 147 | * @return Movie 148 | */ 149 | public function setDescription(?string $description): self 150 | { 151 | $this->description = $description; 152 | 153 | return $this; 154 | } 155 | 156 | /** 157 | * @return Collection 158 | */ 159 | public function getRoles(): Collection 160 | { 161 | return $this->roles; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Entity/Person.php: -------------------------------------------------------------------------------- 1 | ") 45 | * @Assert\NotBlank() 46 | * @Assert\Date() 47 | * @Serializer\Groups({"Default", "Deserialize"}) 48 | * @Serializer\Expose() 49 | */ 50 | private $dateOfBirth; 51 | 52 | public function getId() 53 | { 54 | return $this->id; 55 | } 56 | 57 | public function getFirstName(): ?string 58 | { 59 | return $this->firstName; 60 | } 61 | 62 | public function setFirstName(string $firstName): self 63 | { 64 | $this->firstName = $firstName; 65 | 66 | return $this; 67 | } 68 | 69 | public function getLastName(): ?string 70 | { 71 | return $this->lastName; 72 | } 73 | 74 | public function setLastName(string $lastName): self 75 | { 76 | $this->lastName = $lastName; 77 | 78 | return $this; 79 | } 80 | 81 | public function getDateOfBirth(): ?\DateTimeInterface 82 | { 83 | return $this->dateOfBirth; 84 | } 85 | 86 | public function setDateOfBirth(\DateTimeInterface $dateOfBirth): self 87 | { 88 | $this->dateOfBirth = $dateOfBirth; 89 | 90 | return $this; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Entity/Role.php: -------------------------------------------------------------------------------- 1 | id; 66 | } 67 | 68 | /** 69 | * @return Person 70 | */ 71 | public function getPerson(): Person 72 | { 73 | return $this->person; 74 | } 75 | 76 | /** 77 | * @param Person $person 78 | */ 79 | public function setPerson(Person $person) 80 | { 81 | $this->person = $person; 82 | } 83 | 84 | /** 85 | * @return string 86 | */ 87 | public function getPlayedName(): string 88 | { 89 | return $this->playedName; 90 | } 91 | 92 | /** 93 | * @param string $playedName 94 | */ 95 | public function setPlayedName(string $playedName) 96 | { 97 | $this->playedName = $playedName; 98 | } 99 | 100 | /** 101 | * @return Movie 102 | */ 103 | public function getMovie(): Movie 104 | { 105 | return $this->movie; 106 | } 107 | 108 | /** 109 | * @param Movie $movie 110 | */ 111 | public function setMovie(Movie $movie) 112 | { 113 | $this->movie = $movie; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Entity/User.php: -------------------------------------------------------------------------------- 1 | 80 | * public function getRoles() 81 | * { 82 | * return array('ROLE_USER'); 83 | * } 84 | * 85 | * 86 | * Alternatively, the roles might be stored on a ``roles`` property, 87 | * and populated in any number of different ways when the user object 88 | * is created. 89 | * 90 | * @return (Role|string)[] The user roles 91 | */ 92 | public function getRoles() 93 | { 94 | return $this->roles; 95 | } 96 | 97 | /** 98 | * @param string $retypedPassword 99 | */ 100 | public function setRetypedPassword(?string $retypedPassword): void 101 | { 102 | $this->retypedPassword = $retypedPassword; 103 | } 104 | 105 | /** 106 | * @param array $roles 107 | */ 108 | public function setRoles(array $roles): void 109 | { 110 | $this->roles = $roles; 111 | } 112 | 113 | /** 114 | * Returns the password used to authenticate the user. 115 | * 116 | * This should be the encoded password. On authentication, a plain-text 117 | * password will be salted, encoded, and then compared to this value. 118 | * 119 | * @return string The password 120 | */ 121 | public function getPassword() 122 | { 123 | return $this->password; 124 | } 125 | 126 | /** 127 | * Returns the salt that was originally used to encode the password. 128 | * 129 | * This can return null if the password was not encoded using a salt. 130 | * 131 | * @return string|null The salt 132 | */ 133 | public function getSalt() 134 | { 135 | 136 | } 137 | 138 | /** 139 | * Returns the username used to authenticate the user. 140 | * 141 | * @return string The username 142 | */ 143 | public function getUsername() 144 | { 145 | return $this->username; 146 | } 147 | 148 | /** 149 | * Removes sensitive data from the user. 150 | * 151 | * This is important if, at any given point, sensitive information like 152 | * the plain-text password is stored on this object. 153 | */ 154 | public function eraseCredentials() 155 | { 156 | 157 | } 158 | 159 | /** 160 | * @return int $id 161 | */ 162 | public function getId(): ?int 163 | { 164 | return $this->id; 165 | } 166 | 167 | /** 168 | * @param string $username 169 | * @return $this 170 | */ 171 | public function setUsername(string $username) 172 | { 173 | $this->username = $username; 174 | return $this; 175 | } 176 | 177 | /** 178 | * @param string $password 179 | */ 180 | public function setPassword(?string $password): void 181 | { 182 | $this->password = $password; 183 | } 184 | 185 | /** 186 | * @return string 187 | */ 188 | public function getRetypedPassword(): ?string 189 | { 190 | return $this->retypedPassword; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/Exception/ValidationException.php: -------------------------------------------------------------------------------- 1 | getPropertyPath()] = $violation->getMessage(); 17 | } 18 | 19 | parent::__construct(400, json_encode($message)); 20 | 21 | } 22 | } -------------------------------------------------------------------------------- /src/Kernel.php: -------------------------------------------------------------------------------- 1 | getProjectDir().'/var/cache/'.$this->environment; 21 | } 22 | 23 | public function getLogDir() 24 | { 25 | return $this->getProjectDir().'/var/log'; 26 | } 27 | 28 | public function registerBundles() 29 | { 30 | $contents = require $this->getProjectDir().'/config/bundles.php'; 31 | foreach ($contents as $class => $envs) { 32 | if (isset($envs['all']) || isset($envs[$this->environment])) { 33 | yield new $class(); 34 | } 35 | } 36 | } 37 | 38 | protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader) 39 | { 40 | $container->addResource(new FileResource($this->getProjectDir().'/config/bundles.php')); 41 | // Feel free to remove the "container.autowiring.strict_mode" parameter 42 | // if you are using symfony/dependency-injection 4.0+ as it's the default behavior 43 | $container->setParameter('container.autowiring.strict_mode', true); 44 | $container->setParameter('container.dumper.inline_class_loader', true); 45 | $confDir = $this->getProjectDir().'/config'; 46 | 47 | $loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob'); 48 | $loader->load($confDir.'/{packages}/'.$this->environment.'/**/*'.self::CONFIG_EXTS, 'glob'); 49 | $loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob'); 50 | $loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob'); 51 | } 52 | 53 | protected function configureRoutes(RouteCollectionBuilder $routes) 54 | { 55 | $confDir = $this->getProjectDir().'/config'; 56 | 57 | $routes->import($confDir.'/{routes}/*'.self::CONFIG_EXTS, '/', 'glob'); 58 | $routes->import($confDir.'/{routes}/'.$this->environment.'/**/*'.self::CONFIG_EXTS, '/', 'glob'); 59 | $routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob'); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Migrations/.gitignore: -------------------------------------------------------------------------------- 1 | * -------------------------------------------------------------------------------- /src/Repository/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neverovski/symfony-restful-api/4510c4a9f5d2ce6a1bb7554f8d976c98c6a37239/src/Repository/.gitignore -------------------------------------------------------------------------------- /src/Repository/ImageRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('m'); 29 | 30 | return $qb->select('count(m.id)') 31 | ->getQuery() 32 | ->getSingleScalarResult(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Repository/PersonRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('m'); 29 | 30 | return $qb->select('count(m.id)') 31 | ->getQuery() 32 | ->getSingleScalarResult(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Repository/RoleRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('r'); 24 | return $qb->select('count(r.id)') 25 | ->where('r.movie = :movieId') 26 | ->setParameter('movieId', $movieId) 27 | ->getQuery() 28 | ->getSingleScalarResult(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Repository/UserRepository.php: -------------------------------------------------------------------------------- 1 | getParameters(), 16 | array_flip($this->getQueryParamsBlacklist()) 17 | ); 18 | } 19 | 20 | /** 21 | * @return array 22 | */ 23 | public function getQueryParamsBlacklist(): array 24 | { 25 | return self::QUERY_PARAMS_BLACKLIST; 26 | } 27 | } -------------------------------------------------------------------------------- /src/Resource/Filtering/AbstractFilterDefinitionFactory.php: -------------------------------------------------------------------------------- 1 | 'desc'], 21 | explode( 22 | ' ', 23 | preg_replace('/\s+/', ' ', $item) 24 | ) 25 | ); 26 | $carry[$by] = $order; 27 | 28 | return $carry; 29 | }, 30 | [] 31 | ), array_flip($this->getAcceptedSortField())); 32 | } 33 | } -------------------------------------------------------------------------------- /src/Resource/Filtering/FilterDefinitionFactoryInterface.php: -------------------------------------------------------------------------------- 1 | title = $title; 69 | $this->yearFrom = $yearFrom; 70 | $this->yearTo = $yearTo; 71 | $this->timeFrom = $timeFrom; 72 | $this->timeTo = $timeTo; 73 | $this->sortBy = $sortByQuery; 74 | $this->sortByArray = $sortByArray; 75 | } 76 | 77 | /** 78 | * @return null|string 79 | */ 80 | public function getTitle(): ?string 81 | { 82 | return $this->title; 83 | } 84 | 85 | /** 86 | * @return int|null 87 | */ 88 | public function getYearFrom(): ?int 89 | { 90 | return $this->yearFrom; 91 | } 92 | 93 | /** 94 | * @return int|null 95 | */ 96 | public function getYearTo(): ?int 97 | { 98 | return $this->yearTo; 99 | } 100 | 101 | /** 102 | * @return int|null 103 | */ 104 | public function getTimeFrom(): ?int 105 | { 106 | return $this->timeFrom; 107 | } 108 | 109 | /** 110 | * @return int|null 111 | */ 112 | public function getTimeTo(): ?int 113 | { 114 | return $this->timeTo; 115 | } 116 | 117 | /** 118 | * @return array|null 119 | */ 120 | public function getSortByArray(): ?array 121 | { 122 | return $this->sortByArray; 123 | } 124 | 125 | /** 126 | * @return null|string 127 | */ 128 | public function getSortByQuery(): ?string 129 | { 130 | return $this->sortBy; 131 | } 132 | 133 | /** 134 | * @return array 135 | */ 136 | public function getParameters(): array 137 | { 138 | return get_object_vars($this); 139 | } 140 | } -------------------------------------------------------------------------------- /src/Resource/Filtering/Movie/MovieFilterDefinitionFactory.php: -------------------------------------------------------------------------------- 1 | get(self::KEY_TITLE), 30 | $request->get(self::KEY_YEAR_FROM), 31 | $request->get(self::KEY_YEAR_TO), 32 | $request->get(self::KEY_TIME_FROM), 33 | $request->get(self::KEY_TIME_TO), 34 | $request->get(self::KEY_SORT_BY_QUERY), 35 | $this->sortQueryToArray($request->get(self::KEY_SORT_BY_ARRAY)) 36 | ); 37 | } 38 | 39 | /** 40 | * @return array 41 | */ 42 | public function getAcceptedSortField(): array 43 | { 44 | return self::ACCEPTED_SORT_FIELDS; 45 | } 46 | } -------------------------------------------------------------------------------- /src/Resource/Filtering/Movie/MovieResourceFilter.php: -------------------------------------------------------------------------------- 1 | movieRepository = $movieRepository; 23 | } 24 | 25 | /** 26 | * @param MovieFilterDefinition $filter 27 | * @return QueryBuilder 28 | */ 29 | public function getResources($filter): QueryBuilder 30 | { 31 | $qb = $this->getQuery($filter); 32 | $qb->select('movie'); 33 | 34 | return $qb; 35 | } 36 | 37 | /** 38 | * @param MovieFilterDefinition $filter 39 | * @return QueryBuilder 40 | */ 41 | public function getResourceCount($filter): QueryBuilder 42 | { 43 | $qb = $this->getQuery($filter, 'count'); 44 | $qb->select('count(movie)'); 45 | 46 | return $qb; 47 | } 48 | 49 | /** 50 | * @param MovieFilterDefinition $filter 51 | * @param null|string $count 52 | * @return QueryBuilder 53 | */ 54 | public function getQuery(MovieFilterDefinition $filter, ?string $count = null): QueryBuilder 55 | { 56 | $qb = $this->movieRepository->createQueryBuilder('movie'); 57 | 58 | if (null !== $filter->getTitle()) { 59 | $qb->where( 60 | $qb->expr()->like('movie.title', ':title') 61 | ); 62 | $qb->setParameter('title', "%{$filter->getTitle()}%"); 63 | } 64 | 65 | if (null !== $filter->getYearFrom()) { 66 | $qb->andWhere( 67 | $qb->expr()->gte('movie.year', ':yearFrom') 68 | ); 69 | $qb->setParameter('yearFrom', $filter->getYearFrom()); 70 | } 71 | 72 | if (null !== $filter->getYearTo()) { 73 | $qb->andWhere( 74 | $qb->expr()->lte('movie.year', ':yearTo') 75 | ); 76 | $qb->setParameter('yearTo', $filter->getYearTo()); 77 | } 78 | 79 | if (null !== $filter->getTimeFrom()) { 80 | $qb->andWhere( 81 | $qb->expr()->gte('movie.time', ':timeFrom') 82 | ); 83 | $qb->setParameter('timeFrom', $filter->getTimeFrom()); 84 | } 85 | 86 | if (null !== $filter->getTimeTo()) { 87 | $qb->andWhere( 88 | $qb->expr()->lte('movie.time', ':timeTo') 89 | ); 90 | $qb->setParameter('timeTo', $filter->getTimeTo()); 91 | } 92 | 93 | if (null !== $filter->getSortByArray() && $count === null) { 94 | foreach ($filter->getSortByArray() as $by => $order) { 95 | $expr = 'desc' == $order 96 | ? $qb->expr()->desc("movie.$by") 97 | : $qb->expr()->asc("movie.$by"); 98 | $qb->addOrderBy($expr); 99 | } 100 | } 101 | 102 | return $qb; 103 | } 104 | 105 | } -------------------------------------------------------------------------------- /src/Resource/Filtering/Person/PersonFilterDefinition.php: -------------------------------------------------------------------------------- 1 | firstName = $firstName; 62 | $this->lastName = $lastName; 63 | $this->birthFrom = $birthFrom; 64 | $this->birthTo = $birthTo; 65 | $this->sortBy = $sortByQuery; 66 | $this->sortByArray = $sortByArray; 67 | } 68 | 69 | /** 70 | * @return null|string 71 | */ 72 | public function getFirstName(): ?string 73 | { 74 | return $this->firstName; 75 | } 76 | 77 | /** 78 | * @return int|null 79 | */ 80 | public function getLastName(): ?int 81 | { 82 | return $this->lastName; 83 | } 84 | 85 | /** 86 | * @return int|null 87 | */ 88 | public function getBirthFrom(): ?int 89 | { 90 | return $this->birthFrom; 91 | } 92 | 93 | /** 94 | * @return int|null 95 | */ 96 | public function getBirthTo(): ?int 97 | { 98 | return $this->birthTo; 99 | } 100 | 101 | /** 102 | * @return array 103 | */ 104 | public function getParameters(): array 105 | { 106 | return get_object_vars($this); 107 | } 108 | 109 | /** 110 | * @return array|null 111 | */ 112 | public function getSortByArray(): ?array 113 | { 114 | return $this->sortByArray; 115 | } 116 | 117 | /** 118 | * @return null|string 119 | */ 120 | public function getSortByQuery(): ?string 121 | { 122 | return $this->sortBy; 123 | } 124 | } -------------------------------------------------------------------------------- /src/Resource/Filtering/Person/PersonFilterDefinitionFactory.php: -------------------------------------------------------------------------------- 1 | get(self::KEY_FIRST_NAME), 29 | $request->get(self::KEY_LAST_NAME), 30 | $request->get(self::KEY_BIRTH_FROM), 31 | $request->get(self::KEY_BIRTH_TO), 32 | $request->get(self::KEY_SORT_BY_QUERY), 33 | $this->sortQueryToArray($request->get(self::KEY_SORT_BY_ARRAY)) 34 | ); 35 | } 36 | 37 | /** 38 | * @return array 39 | */ 40 | public function getAcceptedSortField(): array 41 | { 42 | return self::ACCEPTED_SORT_FIELDS; 43 | } 44 | } -------------------------------------------------------------------------------- /src/Resource/Filtering/Person/PersonResourceFilter.php: -------------------------------------------------------------------------------- 1 | personRepository = $personRepository; 23 | } 24 | 25 | /** 26 | * @param PersonRepository $filter 27 | * @return QueryBuilder 28 | */ 29 | public function getResources($filter): QueryBuilder 30 | { 31 | $qb = $this->getQuery($filter); 32 | $qb->select('person'); 33 | 34 | return $qb; 35 | } 36 | 37 | /** 38 | * @param PersonRepository $filter 39 | * @return QueryBuilder 40 | */ 41 | public function getResourceCount($filter): QueryBuilder 42 | { 43 | $qb = $this->getQuery($filter, 'count'); 44 | $qb->select('count(person)'); 45 | 46 | return $qb; 47 | } 48 | 49 | /** 50 | * @param PersonFilterDefinition $filter 51 | * @param null|string $count 52 | * @return QueryBuilder 53 | */ 54 | public function getQuery(PersonFilterDefinition $filter, ?string $count = null): QueryBuilder 55 | { 56 | $qb = $this->personRepository->createQueryBuilder('person'); 57 | 58 | if (null !== $filter->getLastName()) { 59 | $qb->where( 60 | $qb->expr()->like('person.lastName', ':lastName') 61 | ); 62 | $qb->setParameter('lastName', "%{$filter->getLastName()}%"); 63 | } 64 | 65 | if (null !== $filter->getFirstName()) { 66 | $qb->andWhere( 67 | $qb->expr()->like('person.firstName', ':firstName') 68 | ); 69 | $qb->setParameter('firstName', "%{$filter->getFirstName()}%"); 70 | } 71 | 72 | if (null !== $filter->getBirthFrom()) { 73 | $qb->andWhere( 74 | $qb->expr()->gte('person.dateOfBirth', ':birthFrom') 75 | ); 76 | $qb->setParameter('birthFrom', $filter->getBirthFrom()); 77 | } 78 | 79 | if (null !== $filter->getBirthTo()) { 80 | $qb->andWhere( 81 | $qb->expr()->lte('person.dateOfBirth', ':birthTo') 82 | ); 83 | $qb->setParameter('birthTo', $filter->getBirthTo()); 84 | } 85 | 86 | if (null !== $filter->getSortByArray() && $count === null) { 87 | foreach ($filter->getSortByArray() as $by => $order) { 88 | $expr = 'desc' == $order 89 | ? $qb->expr()->desc("person.$by") 90 | : $qb->expr()->asc("person.$by"); 91 | $qb->addOrderBy($expr); 92 | } 93 | } 94 | 95 | return $qb; 96 | } 97 | 98 | } -------------------------------------------------------------------------------- /src/Resource/Filtering/ResourceFilterInterface.php: -------------------------------------------------------------------------------- 1 | playedName = $playedName; 48 | $this->movie = $movie; 49 | $this->sortBy = $sortByQuery; 50 | $this->sortByArray = $sortByArray; 51 | } 52 | 53 | /** 54 | * @return null|string 55 | */ 56 | public function getPlayedName(): ?string 57 | { 58 | return $this->playedName; 59 | } 60 | 61 | 62 | /** 63 | * @return int|null 64 | */ 65 | public function getMovie(): ?int 66 | { 67 | return $this->movie; 68 | } 69 | 70 | /** 71 | * @return null|string 72 | */ 73 | public function getSortByQuery(): ?string 74 | { 75 | return $this->sortBy; 76 | } 77 | 78 | /** 79 | * @return array|null 80 | */ 81 | public function getSortByArray(): ?array 82 | { 83 | return $this->sortByArray; 84 | } 85 | 86 | /** 87 | * @return array 88 | */ 89 | public function getParameters(): array 90 | { 91 | return get_object_vars($this); 92 | } 93 | } -------------------------------------------------------------------------------- /src/Resource/Filtering/Role/RoleFilterDefinitionFactory.php: -------------------------------------------------------------------------------- 1 | get(self::KEY_PLAYED_NAME), 28 | $movie, 29 | $request->get(self::KEY_SORT_BY_QUERY), 30 | $this->sortQueryToArray($request->get(self::KEY_SORT_BY_ARRAY)) 31 | ); 32 | } 33 | 34 | /** 35 | * @return array 36 | */ 37 | public function getAcceptedSortField(): array 38 | { 39 | return self::ACCEPTED_SORT_FIELDS; 40 | } 41 | } -------------------------------------------------------------------------------- /src/Resource/Filtering/Role/RoleResourceFilter.php: -------------------------------------------------------------------------------- 1 | roleRepository = $roleRepository; 23 | } 24 | 25 | /** 26 | * @param $filter 27 | * @return QueryBuilder 28 | */ 29 | public function getResources($filter): QueryBuilder 30 | { 31 | $qb = $this->getQuery($filter); 32 | $qb->select('role'); 33 | 34 | return $qb; 35 | } 36 | 37 | /** 38 | * @param $filter 39 | * @return QueryBuilder 40 | */ 41 | public function getResourceCount($filter): QueryBuilder 42 | { 43 | $qb = $this->getQuery($filter, 'count'); 44 | $qb->select('count(role)'); 45 | 46 | return $qb; 47 | } 48 | 49 | public function getQuery(RoleFilterDefinition $filter, ?string $count = null): QueryBuilder 50 | { 51 | $qb = $this->roleRepository->createQueryBuilder('role'); 52 | 53 | if (null !== $filter->getPlayedName()) { 54 | $qb->where( 55 | $qb->expr()->like('role.playedName', ':playedName') 56 | ); 57 | $qb->setParameter('playedName', "%{$filter->getPlayedName()}%"); 58 | } 59 | 60 | if (null !== $filter->getMovie()) { 61 | $qb->andWhere( 62 | $qb->expr()->eq('role.movie', ':movieId') 63 | ); 64 | $qb->setParameter('movieId', $filter->getMovie()); 65 | } 66 | 67 | if (null !== $filter->getSortByArray() && $count === null) { 68 | foreach ($filter->getSortByArray() as $by => $order) { 69 | $expr = 'desc' == $order 70 | ? $qb->expr()->desc("role.$by") 71 | : $qb->expr()->asc("role.$by"); 72 | $qb->addOrderBy($expr); 73 | } 74 | } 75 | 76 | return $qb; 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /src/Resource/Filtering/SortTableFilterDefinitionInterface.php: -------------------------------------------------------------------------------- 1 | getResourceFilter()->getResources($filter) 20 | ->setFirstResult($page->getOffset()) 21 | ->setMaxResults($page->getLimit()) 22 | ->getQuery() 23 | ->getResult(); 24 | 25 | $resourceCount = $pages = null; 26 | 27 | try { 28 | $resourceCount = $this->getResourceFilter()->getResourceCount($filter) 29 | ->getQuery() 30 | ->getSingleScalarResult(); 31 | $pages = ceil($resourceCount / $page->getLimit()); 32 | } catch (UnexpectedResultException $e) { 33 | 34 | } 35 | 36 | return new PaginatedRepresentation( 37 | new CollectionRepresentation($resources), 38 | $this->getRouteName(), 39 | $filter->getQueryParameters(), 40 | $page->getPage(), 41 | $page->getLimit(), 42 | $pages, 43 | null, 44 | null, 45 | false, 46 | $resourceCount 47 | ); 48 | } 49 | } -------------------------------------------------------------------------------- /src/Resource/Pagination/Movie/MoviePagination.php: -------------------------------------------------------------------------------- 1 | resourceFilter = $resourceFilter; 28 | } 29 | 30 | /** 31 | * @return ResourceFilterInterface 32 | */ 33 | public function getResourceFilter(): ResourceFilterInterface 34 | { 35 | return $this->resourceFilter; 36 | } 37 | 38 | /** 39 | * @return string 40 | */ 41 | public function getRouteName(): string 42 | { 43 | return self::ROUTE; 44 | } 45 | } -------------------------------------------------------------------------------- /src/Resource/Pagination/Page.php: -------------------------------------------------------------------------------- 1 | page = $page; 30 | $this->limit = $limit; 31 | $this->offset = ($page - 1) * $limit; 32 | } 33 | 34 | /** 35 | * @return int 36 | */ 37 | public function getPage(): int 38 | { 39 | return $this->page; 40 | } 41 | 42 | /** 43 | * @return int 44 | */ 45 | public function getLimit(): int 46 | { 47 | return $this->limit; 48 | } 49 | 50 | /** 51 | * @return float|int 52 | */ 53 | public function getOffset() 54 | { 55 | return $this->offset; 56 | } 57 | 58 | 59 | } -------------------------------------------------------------------------------- /src/Resource/Pagination/PageRequestFactory.php: -------------------------------------------------------------------------------- 1 | get(self::KEY_LIMIT, self::DEFAULT_LIMIT); 17 | $page = $request->get(self::KEY_PAGE, self::DEFAULT_PAGE); 18 | 19 | return new Page($page, $limit); 20 | } 21 | } -------------------------------------------------------------------------------- /src/Resource/Pagination/PaginationInterface.php: -------------------------------------------------------------------------------- 1 | resourceFilter = $resourceFilter; 28 | } 29 | /** 30 | * @return ResourceFilterInterface 31 | */ 32 | public function getResourceFilter(): ResourceFilterInterface 33 | { 34 | return $this->resourceFilter; 35 | } 36 | 37 | /** 38 | * @return string 39 | */ 40 | public function getRouteName(): string 41 | { 42 | return self::ROUTE; 43 | } 44 | } -------------------------------------------------------------------------------- /src/Resource/Pagination/Role/RolePagination.php: -------------------------------------------------------------------------------- 1 | resourceFilter = $resourceFilter; 29 | } 30 | 31 | /** 32 | * @return ResourceFilterInterface 33 | */ 34 | public function getResourceFilter(): ResourceFilterInterface 35 | { 36 | return $this->resourceFilter; 37 | } 38 | 39 | /** 40 | * @return string 41 | */ 42 | public function getRouteName(): string 43 | { 44 | return self::ROUTE; 45 | } 46 | } -------------------------------------------------------------------------------- /src/Security/TokenAuthenticator.php: -------------------------------------------------------------------------------- 1 | jwtEncode = $jwtEncode; 40 | $this->tokenStorage = $tokenStorage; 41 | } 42 | 43 | /** 44 | * @param Request $request 45 | * @param AuthenticationException|null $authException 46 | * @return JsonResponse|Response 47 | */ 48 | public function start(Request $request, AuthenticationException $authException = null) 49 | { 50 | $data = array( 51 | // you might translate this message 52 | 'message' => 'Authentication Required' 53 | ); 54 | 55 | return new JsonResponse($data, Response::HTTP_UNAUTHORIZED); 56 | } 57 | 58 | /** 59 | * Does the authenticator support the given Request? 60 | * 61 | * If this returns false, the authenticator will be skipped. 62 | * 63 | * @param Request $request 64 | * 65 | * @return bool 66 | */ 67 | public function supports(Request $request) 68 | { 69 | $extractor = new AuthorizationHeaderTokenExtractor('Bearer', 'Authorization'); 70 | return $extractor->extract($request); 71 | } 72 | 73 | /** 74 | * Get the authentication credentials from the request and return them 75 | * as any type (e.g. an associate array). 76 | * 77 | * Whatever value you return here will be passed to getUser() and checkCredentials() 78 | * 79 | * For example, for a form login, you might: 80 | * 81 | * return array( 82 | * 'username' => $request->request->get('_username'), 83 | * 'password' => $request->request->get('_password'), 84 | * ); 85 | * 86 | * Or for an API token that's on a header, you might use: 87 | * 88 | * return array('api_key' => $request->headers->get('X-API-TOKEN')); 89 | * 90 | * @param Request $request 91 | * 92 | * @return mixed Any non-null value 93 | * 94 | * @throws \UnexpectedValueException If null is returned 95 | */ 96 | public function getCredentials(Request $request) 97 | { 98 | $extractor = new AuthorizationHeaderTokenExtractor('Bearer', 'Authorization'); 99 | $token = $extractor->extract($request); 100 | 101 | if (!$token) { 102 | return null; 103 | } 104 | return array( 105 | 'token' => $token, 106 | ); 107 | } 108 | 109 | /** 110 | * Return a UserInterface object based on the credentials. 111 | * 112 | * The *credentials* are the return value from getCredentials() 113 | * 114 | * You may throw an AuthenticationException if you wish. If you return 115 | * null, then a UsernameNotFoundException is thrown for you. 116 | * 117 | * @param mixed $credentials 118 | * @param UserProviderInterface $userProvider 119 | * @return UserInterface|null 120 | * @throws \Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException 121 | */ 122 | public function getUser($credentials, UserProviderInterface $userProvider) 123 | { 124 | try { 125 | $data = $this->jwtEncode->decode($credentials['token']); 126 | 127 | if (false === $data) { 128 | return null; 129 | } 130 | 131 | if ($this->tokenStorage->isTokenValid($data['username'], $credentials['token'])) { 132 | return null; 133 | } 134 | return $userProvider->loadUserByUsername($data['username']); 135 | 136 | } catch (JWTEncodeFailureException $exception) { 137 | return null; 138 | } 139 | 140 | } 141 | 142 | /** 143 | * Returns true if the credentials are valid. 144 | * 145 | * If any value other than true is returned, authentication will 146 | * fail. You may also throw an AuthenticationException if you wish 147 | * to cause authentication to fail. 148 | * 149 | * The *credentials* are the return value from getCredentials() 150 | * 151 | * @param mixed $credentials 152 | * @param UserInterface $user 153 | * 154 | * @return bool 155 | * 156 | * @throws AuthenticationException 157 | */ 158 | public function checkCredentials($credentials, UserInterface $user) 159 | { 160 | // check credentials - e.g. make sure the password is valid 161 | // no credential check is needed in this case 162 | 163 | // return true to cause authentication success 164 | return true; 165 | } 166 | 167 | /** 168 | * Called when authentication executed, but failed (e.g. wrong username password). 169 | * 170 | * This should return the Response sent back to the user, like a 171 | * RedirectResponse to the login page or a 403 response. 172 | * 173 | * If you return null, the request will continue, but the user will 174 | * not be authenticated. This is probably not what you want to do. 175 | * 176 | * @param Request $request 177 | * @param AuthenticationException $exception 178 | * 179 | * @return Response|null 180 | */ 181 | public function onAuthenticationFailure(Request $request, AuthenticationException $exception) 182 | { 183 | $data = array( 184 | 'message' => strtr($exception->getMessageKey(), $exception->getMessageData()) 185 | 186 | // or to translate this message 187 | // $this->translator->trans($exception->getMessageKey(), $exception->getMessageData()) 188 | ); 189 | 190 | return new JsonResponse($data, Response::HTTP_FORBIDDEN); 191 | } 192 | 193 | /** 194 | * Called when authentication executed and was successful! 195 | * 196 | * This should return the Response sent back to the user, like a 197 | * RedirectResponse to the last page they visited. 198 | * 199 | * If you return null, the current request will continue, and the user 200 | * will be authenticated. This makes sense, for example, with an API. 201 | * 202 | * @param Request $request 203 | * @param TokenInterface $token 204 | * @param string $providerKey The provider (i.e. firewall) key 205 | * 206 | * @return Response|null 207 | */ 208 | public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) 209 | { 210 | // on success, let the request continue 211 | return null; 212 | } 213 | 214 | /** 215 | * Does this method support remember me cookies? 216 | * 217 | * Remember me cookie will be set if *all* of the following are met: 218 | * A) This method returns true 219 | * B) The remember_me key under your firewall is configured 220 | * C) The "remember me" functionality is activated. This is usually 221 | * done by having a _remember_me checkbox in your form, but 222 | * can be configured by the "always_remember_me" and "remember_me_parameter" 223 | * parameters under the "remember_me" firewall key 224 | * D) The onAuthenticationSuccess method returns a Response object 225 | * 226 | * @return bool 227 | */ 228 | public function supportsRememberMe() 229 | { 230 | return false; 231 | } 232 | 233 | } -------------------------------------------------------------------------------- /src/Security/TokenStorage.php: -------------------------------------------------------------------------------- 1 | redisClient = $redisClient; 23 | } 24 | 25 | /** 26 | * @param string $username 27 | * @param string $token 28 | */ 29 | public function storeToken(string $username, string $token): void 30 | { 31 | $this->redisClient->set( 32 | $username.self::KEY_SUFFIX, 33 | $token 34 | ); 35 | $this->redisClient->expire( 36 | $username.self::KEY_SUFFIX, 37 | 3600 38 | ); 39 | } 40 | 41 | /** 42 | * @param string $username 43 | */ 44 | public function invalidateToken(string $username): void 45 | { 46 | $this->redisClient->del($username.self::KEY_SUFFIX); 47 | } 48 | 49 | /** 50 | * @param string $username 51 | * @param string $token 52 | * @return bool 53 | */ 54 | public function isTokenValid(string $username, string $token): bool 55 | { 56 | return $this->redisClient->get($username.self::KEY_SUFFIX) === $token; 57 | } 58 | } -------------------------------------------------------------------------------- /src/Security/UserVoter.php: -------------------------------------------------------------------------------- 1 | decisionManager = $decisionManager; 27 | } 28 | 29 | /** 30 | * Determines if the attribute and subject are supported by this voter. 31 | * 32 | * @param string $attribute An attribute 33 | * @param mixed $subject The subject to secure, e.g. an object the user wants to access or any other PHP type 34 | * 35 | * @return bool True if the attribute and subject are supported, false otherwise 36 | */ 37 | protected function supports($attribute, $subject) 38 | { 39 | if (!in_array($attribute, [self::SHOW, self::EDIT])) { 40 | return false; 41 | } 42 | 43 | if (!$subject instanceof User) { 44 | return false; 45 | } 46 | 47 | return true; 48 | } 49 | 50 | /** 51 | * Perform a single access check operation on a given attribute, subject and token. 52 | * It is safe to assume that $attribute and $subject already passed the "supports()" method check. 53 | * 54 | * @param string $attribute 55 | * @param mixed $subject 56 | * @param TokenInterface $token 57 | * 58 | * @return bool 59 | */ 60 | protected function voteOnAttribute($attribute, $subject, TokenInterface $token) 61 | { 62 | if ($this->decisionManager->decide($token, [User::ROLE_ADMIN])) { 63 | return true; 64 | } 65 | switch ($attribute) { 66 | case self::SHOW: 67 | case self::EDIT: 68 | return $this->isUserHimself($subject, $token); 69 | } 70 | 71 | return false; 72 | } 73 | 74 | /** 75 | * @param $subject 76 | * @param TokenInterface $token 77 | * @return bool 78 | */ 79 | protected function isUserHimself($subject, TokenInterface $token): bool 80 | { 81 | $authenticateUser = $token->getUser(); 82 | 83 | if (!$authenticateUser instanceof User) { 84 | return false; 85 | } 86 | 87 | /** @var User $user */ 88 | $user = $subject; 89 | 90 | return $authenticateUser->getId() === $user->getId(); 91 | } 92 | } -------------------------------------------------------------------------------- /src/Serializer/DoctrineEntityDeserializationSubscriber.php: -------------------------------------------------------------------------------- 1 | reader = $reader; 28 | $this->doctrineRegistry = $doctrineRegistry; 29 | } 30 | 31 | public static function getSubscribedEvents() 32 | { 33 | return [ 34 | [ 35 | 'event' => 'serializer.pre_deserialize', 36 | 'method' => 'onPreDeserialize', 37 | 'format' => 'json' 38 | ], 39 | [ 40 | 'event' => 'serializer.post_deserialize', 41 | 'method' => 'onPostDeserialize', 42 | 'format' => 'json' 43 | ] 44 | ]; 45 | } 46 | 47 | public function onPreDeserialize(PreDeserializeEvent $event) 48 | { 49 | $deserializeType = $event->getType()['name']; 50 | if(!class_exists($deserializeType)) { 51 | return; 52 | } 53 | $data = $event ->getData(); 54 | $class = new \ReflectionClass($deserializeType); 55 | 56 | foreach ($class->getProperties() as $property) { 57 | if (!isset($data[$property->name])) { 58 | continue; 59 | } 60 | 61 | $annotation = $this->reader->getPropertyAnnotation( 62 | $property, 63 | DeserializeEntity::class 64 | ); 65 | if (null === $annotation || !class_exists($annotation->type)) { 66 | continue; 67 | } 68 | 69 | $data[$property->name] = [ 70 | $annotation->idField => $data[$property->name] 71 | ]; 72 | } 73 | $event->setData($data); 74 | } 75 | 76 | public function onPostDeserialize(ObjectEvent $event) 77 | { 78 | $deserializeType = $event->getType()['name']; 79 | 80 | if(!class_exists($deserializeType)) { 81 | return; 82 | } 83 | $object = $event->getObject(); 84 | $reflection = new \ReflectionObject($object); 85 | 86 | foreach ($reflection->getProperties() as $property) { 87 | 88 | $annotation = $this->reader->getPropertyAnnotation( 89 | $property, 90 | DeserializeEntity::class 91 | ); 92 | if (null === $annotation || !class_exists($annotation->type)) { 93 | continue; 94 | } 95 | 96 | if(!$reflection->hasMethod($annotation->setter)) { 97 | throw new \LogicException( 98 | "Object {$reflection->getName()} does not have the {$annotation->setter} method" 99 | ); 100 | } 101 | 102 | $property->setAccessible(true); 103 | $deserializedEntity = $property->getValue($object); 104 | 105 | if (null === $deserializedEntity) { 106 | return; 107 | } 108 | 109 | $entityId = $deserializedEntity->{$annotation->idGetter}(); 110 | $repository = $this->doctrineRegistry->getRepository($annotation->type); 111 | 112 | $entity = $repository->find($entityId); 113 | if (null === $entity) { 114 | throw new NotFoundHttpException( 115 | "Resource {$reflection->getShortName()}/$entityId" 116 | ); 117 | } 118 | 119 | $object->{$annotation->setter}($entity); 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /symfony.lock: -------------------------------------------------------------------------------- 1 | { 2 | "clue/stream-filter": { 3 | "version": "v1.4.0" 4 | }, 5 | "composer/ca-bundle": { 6 | "version": "1.1.1" 7 | }, 8 | "doctrine/annotations": { 9 | "version": "1.0", 10 | "recipe": { 11 | "repo": "github.com/symfony/recipes", 12 | "branch": "master", 13 | "version": "1.0", 14 | "ref": "cb4152ebcadbe620ea2261da1a1c5a9b8cea7672" 15 | } 16 | }, 17 | "doctrine/cache": { 18 | "version": "v1.7.1" 19 | }, 20 | "doctrine/collections": { 21 | "version": "v1.5.0" 22 | }, 23 | "doctrine/common": { 24 | "version": "v2.8.1" 25 | }, 26 | "doctrine/data-fixtures": { 27 | "version": "v1.3.0" 28 | }, 29 | "doctrine/dbal": { 30 | "version": "v2.6.3" 31 | }, 32 | "doctrine/doctrine-bundle": { 33 | "version": "1.6", 34 | "recipe": { 35 | "repo": "github.com/symfony/recipes", 36 | "branch": "master", 37 | "version": "1.6", 38 | "ref": "c745b67e4dec2771d4d57a60efd224faf445c929" 39 | } 40 | }, 41 | "doctrine/doctrine-cache-bundle": { 42 | "version": "1.3.3" 43 | }, 44 | "doctrine/doctrine-fixtures-bundle": { 45 | "version": "3.0", 46 | "recipe": { 47 | "repo": "github.com/symfony/recipes", 48 | "branch": "master", 49 | "version": "3.0", 50 | "ref": "2ea6070ecf365f9a801ccaed4b31d4a3b7af5693" 51 | } 52 | }, 53 | "doctrine/doctrine-migrations-bundle": { 54 | "version": "1.2", 55 | "recipe": { 56 | "repo": "github.com/symfony/recipes", 57 | "branch": "master", 58 | "version": "1.2", 59 | "ref": "c1431086fec31f17fbcfe6d6d7e92059458facc1" 60 | } 61 | }, 62 | "doctrine/inflector": { 63 | "version": "v1.3.0" 64 | }, 65 | "doctrine/instantiator": { 66 | "version": "1.1.0" 67 | }, 68 | "doctrine/lexer": { 69 | "version": "v1.0.1" 70 | }, 71 | "doctrine/migrations": { 72 | "version": "v1.6.2" 73 | }, 74 | "doctrine/orm": { 75 | "version": "v2.6.1" 76 | }, 77 | "egulias/email-validator": { 78 | "version": "2.1.3" 79 | }, 80 | "exsyst/swagger": { 81 | "version": "v0.4.0" 82 | }, 83 | "friendsofsymfony/http-cache": { 84 | "version": "2.2.1" 85 | }, 86 | "friendsofsymfony/http-cache-bundle": { 87 | "version": "2.2.0" 88 | }, 89 | "friendsofsymfony/rest-bundle": { 90 | "version": "2.2", 91 | "recipe": { 92 | "repo": "github.com/symfony/recipes-contrib", 93 | "branch": "master", 94 | "version": "2.2", 95 | "ref": "258300d52be6ad59b32a888d5ddafbf9638540ff" 96 | } 97 | }, 98 | "fzaninotto/faker": { 99 | "version": "v1.7.1" 100 | }, 101 | "guzzlehttp/guzzle": { 102 | "version": "6.3.2" 103 | }, 104 | "guzzlehttp/promises": { 105 | "version": "v1.3.1" 106 | }, 107 | "guzzlehttp/psr7": { 108 | "version": "1.4.2" 109 | }, 110 | "jdorn/sql-formatter": { 111 | "version": "v1.2.17" 112 | }, 113 | "jms/metadata": { 114 | "version": "1.6.0" 115 | }, 116 | "jms/parser-lib": { 117 | "version": "1.0.0" 118 | }, 119 | "jms/serializer": { 120 | "version": "1.11.0" 121 | }, 122 | "jms/serializer-bundle": { 123 | "version": "2.0", 124 | "recipe": { 125 | "repo": "github.com/symfony/recipes-contrib", 126 | "branch": "master", 127 | "version": "2.0", 128 | "ref": "fe60ce509ef04a3f40da96e3979bc8d9b13b2372" 129 | } 130 | }, 131 | "lexik/jwt-authentication-bundle": { 132 | "version": "2.3", 133 | "recipe": { 134 | "repo": "github.com/symfony/recipes", 135 | "branch": "master", 136 | "version": "2.3", 137 | "ref": "a66e8a7b75a1825cf2414d5dd53c7ed38c8654d1" 138 | } 139 | }, 140 | "myclabs/deep-copy": { 141 | "version": "1.7.0" 142 | }, 143 | "namshi/jose": { 144 | "version": "7.2.3" 145 | }, 146 | "nelmio/alice": { 147 | "version": "v3.3.0" 148 | }, 149 | "nelmio/api-doc-bundle": { 150 | "version": "3.0", 151 | "recipe": { 152 | "repo": "github.com/symfony/recipes-contrib", 153 | "branch": "master", 154 | "version": "3.0", 155 | "ref": "f95712101a2316e607a95ec54629e791fd3ee875" 156 | } 157 | }, 158 | "nelmio/cors-bundle": { 159 | "version": "1.5", 160 | "recipe": { 161 | "repo": "github.com/symfony/recipes", 162 | "branch": "master", 163 | "version": "1.5", 164 | "ref": "7b6cbc842f8cd3d550815247d12294f6f304a8c4" 165 | } 166 | }, 167 | "nikic/php-parser": { 168 | "version": "v4.0.1" 169 | }, 170 | "ocramius/package-versions": { 171 | "version": "1.3.0" 172 | }, 173 | "ocramius/proxy-manager": { 174 | "version": "2.2.0" 175 | }, 176 | "php-http/client-common": { 177 | "version": "1.7.0" 178 | }, 179 | "php-http/discovery": { 180 | "version": "1.4.0" 181 | }, 182 | "php-http/guzzle6-adapter": { 183 | "version": "v1.1.1" 184 | }, 185 | "php-http/httplug": { 186 | "version": "v1.1.0" 187 | }, 188 | "php-http/message": { 189 | "version": "1.6.0" 190 | }, 191 | "php-http/message-factory": { 192 | "version": "v1.0.2" 193 | }, 194 | "php-http/promise": { 195 | "version": "v1.0.0" 196 | }, 197 | "phpcollection/phpcollection": { 198 | "version": "0.5.0" 199 | }, 200 | "phpdocumentor/reflection-common": { 201 | "version": "1.0.1" 202 | }, 203 | "phpdocumentor/reflection-docblock": { 204 | "version": "4.3.0" 205 | }, 206 | "phpdocumentor/type-resolver": { 207 | "version": "0.4.0" 208 | }, 209 | "phpoption/phpoption": { 210 | "version": "1.5.0" 211 | }, 212 | "predis/predis": { 213 | "version": "v1.1.1" 214 | }, 215 | "psr/cache": { 216 | "version": "1.0.1" 217 | }, 218 | "psr/container": { 219 | "version": "1.0.0" 220 | }, 221 | "psr/http-message": { 222 | "version": "1.0.1" 223 | }, 224 | "psr/log": { 225 | "version": "1.0.2" 226 | }, 227 | "psr/simple-cache": { 228 | "version": "1.0.1" 229 | }, 230 | "sensio/framework-extra-bundle": { 231 | "version": "4.0", 232 | "recipe": { 233 | "repo": "github.com/symfony/recipes", 234 | "branch": "master", 235 | "version": "4.0", 236 | "ref": "aaddfdf43cdecd4cf91f992052d76c2cadc04543" 237 | } 238 | }, 239 | "sensiolabs/security-checker": { 240 | "version": "4.0", 241 | "recipe": { 242 | "repo": "github.com/symfony/recipes", 243 | "branch": "master", 244 | "version": "4.0", 245 | "ref": "e65a105bf4cd5b1b79012ba029954927f05922e8" 246 | } 247 | }, 248 | "snc/redis-bundle": { 249 | "version": "2.0", 250 | "recipe": { 251 | "repo": "github.com/symfony/recipes-contrib", 252 | "branch": "master", 253 | "version": "2.0", 254 | "ref": "9ef855ff444add54c2d66bdf3f4b7b2b6a120259" 255 | } 256 | }, 257 | "swiftmailer/swiftmailer": { 258 | "version": "v6.0.2" 259 | }, 260 | "symfony/asset": { 261 | "version": "v4.0.6" 262 | }, 263 | "symfony/cache": { 264 | "version": "v4.0.6" 265 | }, 266 | "symfony/config": { 267 | "version": "v4.0.6" 268 | }, 269 | "symfony/console": { 270 | "version": "3.3", 271 | "recipe": { 272 | "repo": "github.com/symfony/recipes", 273 | "branch": "master", 274 | "version": "3.3", 275 | "ref": "e3868d2f4a5104f19f844fe551099a00c6562527" 276 | } 277 | }, 278 | "symfony/debug": { 279 | "version": "v4.0.6" 280 | }, 281 | "symfony/debug-bundle": { 282 | "version": "3.3", 283 | "recipe": { 284 | "repo": "github.com/symfony/recipes", 285 | "branch": "master", 286 | "version": "3.3", 287 | "ref": "71d29aaf710fd59cd3abff2b1ade907ed73103c6" 288 | } 289 | }, 290 | "symfony/dependency-injection": { 291 | "version": "v4.0.6" 292 | }, 293 | "symfony/doctrine-bridge": { 294 | "version": "v4.0.6" 295 | }, 296 | "symfony/dotenv": { 297 | "version": "v4.0.6" 298 | }, 299 | "symfony/event-dispatcher": { 300 | "version": "v4.0.6" 301 | }, 302 | "symfony/expression-language": { 303 | "version": "v4.0.6" 304 | }, 305 | "symfony/filesystem": { 306 | "version": "v4.0.6" 307 | }, 308 | "symfony/finder": { 309 | "version": "v4.0.6" 310 | }, 311 | "symfony/flex": { 312 | "version": "1.0", 313 | "recipe": { 314 | "repo": "github.com/symfony/recipes", 315 | "branch": "master", 316 | "version": "1.0", 317 | "ref": "cc1afd81841db36fbef982fe56b48ade6716fac4" 318 | } 319 | }, 320 | "symfony/framework-bundle": { 321 | "version": "3.3", 322 | "recipe": { 323 | "repo": "github.com/symfony/recipes", 324 | "branch": "master", 325 | "version": "3.3", 326 | "ref": "8a2f7fa50a528f0aad1d7a87ae3730c981b367ce" 327 | } 328 | }, 329 | "symfony/http-foundation": { 330 | "version": "v4.0.6" 331 | }, 332 | "symfony/http-kernel": { 333 | "version": "v4.0.6" 334 | }, 335 | "symfony/inflector": { 336 | "version": "v4.0.6" 337 | }, 338 | "symfony/lts": { 339 | "version": "4-dev" 340 | }, 341 | "symfony/maker-bundle": { 342 | "version": "1.0", 343 | "recipe": { 344 | "repo": "github.com/symfony/recipes", 345 | "branch": "master", 346 | "version": "1.0", 347 | "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" 348 | } 349 | }, 350 | "symfony/options-resolver": { 351 | "version": "v4.0.6" 352 | }, 353 | "symfony/orm-pack": { 354 | "version": "v1.0.5" 355 | }, 356 | "symfony/polyfill-apcu": { 357 | "version": "v1.7.0" 358 | }, 359 | "symfony/polyfill-mbstring": { 360 | "version": "v1.7.0" 361 | }, 362 | "symfony/polyfill-php72": { 363 | "version": "v1.7.0" 364 | }, 365 | "symfony/process": { 366 | "version": "v4.0.6" 367 | }, 368 | "symfony/property-access": { 369 | "version": "v4.0.6" 370 | }, 371 | "symfony/property-info": { 372 | "version": "v4.0.8" 373 | }, 374 | "symfony/routing": { 375 | "version": "4.0", 376 | "recipe": { 377 | "repo": "github.com/symfony/recipes", 378 | "branch": "master", 379 | "version": "4.0", 380 | "ref": "cda8b550123383d25827705d05a42acf6819fe4e" 381 | } 382 | }, 383 | "symfony/security": { 384 | "version": "v4.0.8" 385 | }, 386 | "symfony/security-bundle": { 387 | "version": "3.3", 388 | "recipe": { 389 | "repo": "github.com/symfony/recipes", 390 | "branch": "master", 391 | "version": "3.3", 392 | "ref": "f8a63faa0d9521526499c0a8f403c9964ecb0527" 393 | } 394 | }, 395 | "symfony/security-core": { 396 | "version": "v4.0.6" 397 | }, 398 | "symfony/swiftmailer-bundle": { 399 | "version": "2.5", 400 | "recipe": { 401 | "repo": "github.com/symfony/recipes", 402 | "branch": "master", 403 | "version": "2.5", 404 | "ref": "3db029c03e452b4a23f7fc45cec7c922c2247eb8" 405 | } 406 | }, 407 | "symfony/templating": { 408 | "version": "v4.0.6" 409 | }, 410 | "symfony/translation": { 411 | "version": "3.3", 412 | "recipe": { 413 | "repo": "github.com/symfony/recipes", 414 | "branch": "master", 415 | "version": "3.3", 416 | "ref": "6bcd6c570c017ea6ae5a7a6a027c929fd3542cd8" 417 | } 418 | }, 419 | "symfony/twig-bridge": { 420 | "version": "v4.0.6" 421 | }, 422 | "symfony/twig-bundle": { 423 | "version": "3.3", 424 | "recipe": { 425 | "repo": "github.com/symfony/recipes", 426 | "branch": "master", 427 | "version": "3.3", 428 | "ref": "f75ac166398e107796ca94cc57fa1edaa06ec47f" 429 | } 430 | }, 431 | "symfony/validator": { 432 | "version": "v4.0.6" 433 | }, 434 | "symfony/var-dumper": { 435 | "version": "v4.0.6" 436 | }, 437 | "symfony/web-profiler-bundle": { 438 | "version": "3.3", 439 | "recipe": { 440 | "repo": "github.com/symfony/recipes", 441 | "branch": "master", 442 | "version": "3.3", 443 | "ref": "6bdfa1a95f6b2e677ab985cd1af2eae35d62e0f6" 444 | } 445 | }, 446 | "symfony/web-server-bundle": { 447 | "version": "3.3", 448 | "recipe": { 449 | "repo": "github.com/symfony/recipes", 450 | "branch": "master", 451 | "version": "3.3", 452 | "ref": "dae9b39fd6717970be7601101ce5aa960bf53d9a" 453 | } 454 | }, 455 | "symfony/yaml": { 456 | "version": "v4.0.6" 457 | }, 458 | "twig/extensions": { 459 | "version": "1.0", 460 | "recipe": { 461 | "repo": "github.com/symfony/recipes", 462 | "branch": "master", 463 | "version": "1.0", 464 | "ref": "4851df0afc426b8f07204379d21fca25b6df5d68" 465 | } 466 | }, 467 | "twig/twig": { 468 | "version": "v2.4.7" 469 | }, 470 | "webmozart/assert": { 471 | "version": "1.3.0" 472 | }, 473 | "willdurand/hateoas": { 474 | "version": "2.12.0" 475 | }, 476 | "willdurand/hateoas-bundle": { 477 | "version": "1.4.0" 478 | }, 479 | "willdurand/jsonp-callback-validator": { 480 | "version": "v1.1.0" 481 | }, 482 | "willdurand/negotiation": { 483 | "version": "v2.3.1" 484 | }, 485 | "zendframework/zend-code": { 486 | "version": "3.3.0" 487 | }, 488 | "zendframework/zend-eventmanager": { 489 | "version": "3.2.0" 490 | }, 491 | "zircote/swagger-php": { 492 | "version": "2.0.13" 493 | } 494 | } 495 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /translations/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neverovski/symfony-restful-api/4510c4a9f5d2ce6a1bb7554f8d976c98c6a37239/translations/.gitignore --------------------------------------------------------------------------------