├── .env ├── .gitignore ├── README.md ├── bin └── console ├── composer.json ├── composer.lock ├── config ├── bundles.php ├── packages │ ├── api_platform.yaml │ ├── bazinga_hateoas.yaml │ ├── cache.yaml │ ├── dev │ │ └── jms_serializer.yaml │ ├── doctrine.yaml │ ├── doctrine_migrations.yaml │ ├── framework.yaml │ ├── jms_serializer.yaml │ ├── lexik_jwt_authentication.yaml │ ├── nelmio_api_doc.yaml │ ├── nelmio_cors.yaml │ ├── prod │ │ └── jms_serializer.yaml │ ├── routing.yaml │ ├── security.yaml │ ├── sensio_framework_extra.yaml │ ├── twig.yaml │ └── validator.yaml ├── preload.php ├── routes.yaml ├── routes │ ├── annotations.yaml │ ├── api_platform.yaml │ ├── framework.yaml │ └── nelmio_api_doc.yaml └── services.yaml ├── migrations └── .gitignore ├── public └── index.php ├── src ├── Controller │ ├── .gitignore │ ├── AuthorController.php │ ├── BookController.php │ └── ExternalApiController.php ├── DataFixtures │ └── AppFixtures.php ├── Entity │ ├── .gitignore │ ├── Author.php │ ├── Book.php │ └── User.php ├── EventSubscriber │ └── ExceptionSubscriber.php ├── Kernel.php ├── OpenApi │ └── JwtDecorator.php ├── Repository │ ├── .gitignore │ ├── AuthorRepository.php │ ├── BookRepository.php │ └── UserRepository.php └── Service │ └── VersioningService.php ├── symfony.lock └── templates └── base.html.twig /.env: -------------------------------------------------------------------------------- 1 | # In all environments, the following files are loaded if they exist, 2 | # the latter taking precedence over the former: 3 | # 4 | # * .env contains default values for the environment variables needed by the app 5 | # * .env.local uncommitted file with local overrides 6 | # * .env.$APP_ENV committed environment-specific defaults 7 | # * .env.$APP_ENV.local uncommitted environment-specific overrides 8 | # 9 | # Real environment variables win over .env files. 10 | # 11 | # DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. 12 | # 13 | # Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). 14 | # https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration 15 | 16 | ###> symfony/framework-bundle ### 17 | APP_ENV=dev 18 | APP_SECRET=7e8c0e2b69462793d7491d962d7afe7b 19 | ###< symfony/framework-bundle ### 20 | 21 | ###> doctrine/doctrine-bundle ### 22 | # Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url 23 | # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml 24 | # 25 | # DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" 26 | # DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7&charset=utf8mb4" 27 | DATABASE_URL="postgresql://symfony:ChangeMe@127.0.0.1:5432/app?serverVersion=13&charset=utf8" 28 | ###< doctrine/doctrine-bundle ### 29 | 30 | ###> lexik/jwt-authentication-bundle ### 31 | JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem 32 | JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem 33 | JWT_PASSPHRASE=VotrePassPhraseIci 34 | ###< lexik/jwt-authentication-bundle ### 35 | 36 | ###> nelmio/cors-bundle ### 37 | CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' 38 | ###< nelmio/cors-bundle ### 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ###> symfony/framework-bundle ### 3 | /.env.local 4 | /.env.local.php 5 | /.env.*.local 6 | /config/secrets/prod/prod.decrypt.private.php 7 | /public/bundles/ 8 | /var/ 9 | /vendor/ 10 | ###< symfony/framework-bundle ### 11 | 12 | ###> lexik/jwt-authentication-bundle ### 13 | /config/jwt/*.pem 14 | ###< lexik/jwt-authentication-bundle ### 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 7709361-API-REST-Symfony 2 | 3 | Ce projet sert de support au cours au cours sur API et Symfony d'Openclassrooms. 4 | Il est réalisé avec Symfony 6 et nécessite à minima PHP8. 5 | 6 | Pour vérifier votre version de php vous pouvez faire : 7 | 8 | - _php -v_ 9 | 10 | 11 | Pour utiliser ce projet, vous pouvez simplement faire un : 12 | 13 | - _git clone https://github.com/OpenClassrooms-Student-Center/7709361-API-REST-Symfony.git_ 14 | 15 | Et une fois le projet récupérez il faudra l'initialiser : 16 | 17 | - _composer install_ : pour récupérer l'ensemble des packages nécessaires 18 | - créer vos clefs publiques et privées pour JWT dans config/jwt : 19 | - créez le répertoire "jwt" dans le dossier config 20 | - _openssl genpkey -out config/jwt/private.pem -aes256 -algorithm rsa -pkeyopt rsa_keygen_bits:4096_ : pour créer la clef privée 21 | - _openssl pkey -in config/jwt/private.pem -out config/jwt/public.pem-pubout_ : pour créer la clef publique 22 | - créer un fichier .env.local 23 | - ce fichier doit contenir vos identifiants de connexion à la base de données 24 | - le chemin vers vos clefs privées et publiques 25 | - votre passphrase de création de clef 26 | - _php bin/console doctrine:database:create_ : pour créer la base de données 27 | - _php bin/console doctrine:schema:update --force_ : pour créer les tables 28 | - _php bin/console doctrine:fixtures:load_ : pour charger les fixtures 29 | 30 | Si _openssl_ ne fonctionne pas, tentez de lancer cette commande depuis un "gitbash". 31 | 32 | Pour tester les routes, vous pouvez les interroger directement via postman. Par exemple : 33 | - https://127.0.0.1:8000/api/login_check : pour se logger 34 | - https://127.0.0.1:8000/api/books : pour récuperer la liste des livres 35 | 36 | Vous pouvez également utiliser la documentation via Nelmio : 37 | - https://127.0.0.1:8000/api/doc 38 | 39 | Vous pouvez également utiliser API Platform : 40 | - https://127.0.0.1:8000/apip 41 | 42 | Chaque branche du projet correspond à un chapitre du cours. 43 | En cas de soucis, référez-vous au cours d'Openclassrooms. 44 | 45 | Bonne chance ! 46 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | =8.0.2", 8 | "ext-ctype": "*", 9 | "ext-iconv": "*", 10 | "api-platform/core": "^2.6", 11 | "doctrine/annotations": "^1.13", 12 | "doctrine/doctrine-bundle": "^2.5", 13 | "doctrine/doctrine-migrations-bundle": "^3.2", 14 | "doctrine/orm": "^2.11", 15 | "lexik/jwt-authentication-bundle": "^2.14", 16 | "nelmio/api-doc-bundle": "^4.8", 17 | "nelmio/cors-bundle": "^2.2", 18 | "phpdocumentor/reflection-docblock": "^5.3", 19 | "phpstan/phpdoc-parser": "^1.2", 20 | "sensio/framework-extra-bundle": "^6.2", 21 | "symfony/asset": "6.0.*", 22 | "symfony/console": "6.0.*", 23 | "symfony/dotenv": "6.0.*", 24 | "symfony/expression-language": "6.0.*", 25 | "symfony/flex": "^2", 26 | "symfony/framework-bundle": "6.0.*", 27 | "symfony/http-client": "6.0.*", 28 | "symfony/property-access": "6.0.*", 29 | "symfony/property-info": "6.0.*", 30 | "symfony/proxy-manager-bridge": "6.0.*", 31 | "symfony/runtime": "6.0.*", 32 | "symfony/security-bundle": "6.0.*", 33 | "symfony/serializer": "6.0.*", 34 | "symfony/twig-bundle": "6.0.*", 35 | "symfony/validator": "6.0.*", 36 | "symfony/yaml": "6.0.*", 37 | "twig/extra-bundle": "^2.12|^3.0", 38 | "twig/twig": "^2.12|^3.0", 39 | "willdurand/hateoas-bundle": "^2.4" 40 | }, 41 | "require-dev": { 42 | "doctrine/doctrine-fixtures-bundle": "^3.4", 43 | "symfony/maker-bundle": "^1.36" 44 | }, 45 | "config": { 46 | "allow-plugins": { 47 | "composer/package-versions-deprecated": true, 48 | "symfony/flex": true, 49 | "symfony/runtime": true 50 | }, 51 | "optimize-autoloader": true, 52 | "preferred-install": { 53 | "*": "dist" 54 | }, 55 | "sort-packages": true 56 | }, 57 | "autoload": { 58 | "psr-4": { 59 | "App\\": "src/" 60 | } 61 | }, 62 | "autoload-dev": { 63 | "psr-4": { 64 | "App\\Tests\\": "tests/" 65 | } 66 | }, 67 | "replace": { 68 | "symfony/polyfill-ctype": "*", 69 | "symfony/polyfill-iconv": "*", 70 | "symfony/polyfill-php72": "*", 71 | "symfony/polyfill-php73": "*", 72 | "symfony/polyfill-php74": "*", 73 | "symfony/polyfill-php80": "*" 74 | }, 75 | "scripts": { 76 | "auto-scripts": { 77 | "cache:clear": "symfony-cmd", 78 | "assets:install %PUBLIC_DIR%": "symfony-cmd" 79 | }, 80 | "post-install-cmd": [ 81 | "@auto-scripts" 82 | ], 83 | "post-update-cmd": [ 84 | "@auto-scripts" 85 | ] 86 | }, 87 | "conflict": { 88 | "symfony/symfony": "*" 89 | }, 90 | "extra": { 91 | "symfony": { 92 | "allow-contrib": false, 93 | "require": "6.0.*" 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], 6 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 7 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 8 | Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], 9 | Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], 10 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], 11 | Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true], 12 | JMS\SerializerBundle\JMSSerializerBundle::class => ['all' => true], 13 | Bazinga\Bundle\HateoasBundle\BazingaHateoasBundle::class => ['all' => true], 14 | Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true], 15 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 16 | Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], 17 | Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], 18 | ApiPlatform\Core\Bridge\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true], 19 | ]; 20 | -------------------------------------------------------------------------------- /config/packages/api_platform.yaml: -------------------------------------------------------------------------------- 1 | api_platform: 2 | mapping: 3 | paths: ['%kernel.project_dir%/src/Entity'] 4 | patch_formats: 5 | json: ['application/merge-patch+json'] 6 | swagger: 7 | versions: [3] 8 | api_keys: 9 | apiKey: 10 | name: Authorization 11 | type: header 12 | -------------------------------------------------------------------------------- /config/packages/bazinga_hateoas.yaml: -------------------------------------------------------------------------------- 1 | # For full configuration see https://github.com/willdurand/BazingaHateoasBundle/blob/master/Resources/doc/index.md#reference-configuration 2 | bazinga_hateoas: 3 | twig_extension: 4 | enabled: true 5 | -------------------------------------------------------------------------------- /config/packages/cache.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | cache: 3 | # Unique name of your app: used to compute stable namespaces for cache keys. 4 | #prefix_seed: your_vendor_name/app_name 5 | 6 | # The "app" cache stores to the filesystem by default. 7 | # The data in this cache should persist between deploys. 8 | # Other options include: 9 | 10 | # Redis 11 | #app: cache.adapter.redis 12 | #default_redis_provider: redis://localhost 13 | 14 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) 15 | #app: cache.adapter.apcu 16 | 17 | # Namespaced pools use the above "app" backend by default 18 | #pools: 19 | #my.dedicated.cache: null 20 | -------------------------------------------------------------------------------- /config/packages/dev/jms_serializer.yaml: -------------------------------------------------------------------------------- 1 | jms_serializer: 2 | visitors: 3 | json_serialization: 4 | options: 5 | - JSON_PRETTY_PRINT 6 | - JSON_UNESCAPED_SLASHES 7 | - JSON_PRESERVE_ZERO_FRACTION 8 | -------------------------------------------------------------------------------- /config/packages/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | url: '%env(resolve:DATABASE_URL)%' 4 | 5 | # IMPORTANT: You MUST configure your server version, 6 | # either here or in the DATABASE_URL env var (see .env file) 7 | #server_version: '13' 8 | orm: 9 | auto_generate_proxy_classes: true 10 | naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware 11 | auto_mapping: true 12 | mappings: 13 | App: 14 | is_bundle: false 15 | dir: '%kernel.project_dir%/src/Entity' 16 | prefix: 'App\Entity' 17 | alias: App 18 | 19 | when@test: 20 | doctrine: 21 | dbal: 22 | # "TEST_TOKEN" is typically set by ParaTest 23 | dbname_suffix: '_test%env(default::TEST_TOKEN)%' 24 | 25 | when@prod: 26 | doctrine: 27 | orm: 28 | auto_generate_proxy_classes: false 29 | query_cache_driver: 30 | type: pool 31 | pool: doctrine.system_cache_pool 32 | result_cache_driver: 33 | type: pool 34 | pool: doctrine.result_cache_pool 35 | 36 | framework: 37 | cache: 38 | pools: 39 | doctrine.result_cache_pool: 40 | adapter: cache.app 41 | doctrine.system_cache_pool: 42 | adapter: cache.system 43 | -------------------------------------------------------------------------------- /config/packages/doctrine_migrations.yaml: -------------------------------------------------------------------------------- 1 | doctrine_migrations: 2 | migrations_paths: 3 | # namespace is arbitrary but should be different from App\Migrations 4 | # as migrations classes should NOT be autoloaded 5 | 'DoctrineMigrations': '%kernel.project_dir%/migrations' 6 | enable_profiler: '%kernel.debug%' 7 | -------------------------------------------------------------------------------- /config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | # see https://symfony.com/doc/current/reference/configuration/framework.html 2 | framework: 3 | secret: '%env(APP_SECRET)%' 4 | #csrf_protection: true 5 | http_method_override: false 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: null 11 | cookie_secure: auto 12 | cookie_samesite: lax 13 | storage_factory_id: session.storage.factory.native 14 | 15 | #esi: true 16 | #fragments: true 17 | php_errors: 18 | log: true 19 | 20 | when@test: 21 | framework: 22 | test: true 23 | session: 24 | storage_factory_id: session.storage.factory.mock_file 25 | -------------------------------------------------------------------------------- /config/packages/jms_serializer.yaml: -------------------------------------------------------------------------------- 1 | jms_serializer: 2 | visitors: 3 | xml_serialization: 4 | format_output: '%kernel.debug%' 5 | property_naming: 6 | id: jms_serializer.identical_property_naming_strategy 7 | # metadata: 8 | # auto_detection: false 9 | # directories: 10 | # any-name: 11 | # namespace_prefix: "My\\FooBundle" 12 | # path: "@MyFooBundle/Resources/config/serializer" 13 | # another-name: 14 | # namespace_prefix: "My\\BarBundle" 15 | # path: "@MyBarBundle/Resources/config/serializer" 16 | -------------------------------------------------------------------------------- /config/packages/lexik_jwt_authentication.yaml: -------------------------------------------------------------------------------- 1 | lexik_jwt_authentication: 2 | secret_key: '%env(resolve:JWT_SECRET_KEY)%' 3 | public_key: '%env(resolve:JWT_PUBLIC_KEY)%' 4 | pass_phrase: '%env(JWT_PASSPHRASE)%' 5 | -------------------------------------------------------------------------------- /config/packages/nelmio_api_doc.yaml: -------------------------------------------------------------------------------- 1 | nelmio_api_doc: 2 | documentation: 3 | info: 4 | title: Books 5 | description: Une API d'OpenClassrooms avec des livres, des autrices et des auteurs ! 6 | version: 2.0.0 7 | paths: 8 | /api/login_check: 9 | post: 10 | tags: 11 | - Token 12 | operationId: postCredentialsItem 13 | summary: Permet d'obtenir le token JWT pour se logger. 14 | requestBody: 15 | description: Crée un nouveau token JWT 16 | content: 17 | application/json: 18 | schema: 19 | $ref: '#/components/schemas/Credentials' 20 | responses: 21 | '200': 22 | description: Récupère le token JWT 23 | content: 24 | application/json: 25 | schema: 26 | $ref: '#/components/schemas/Token' 27 | components: 28 | schemas: 29 | Token: 30 | type: object 31 | properties: 32 | token: 33 | type: string 34 | readOnly: true 35 | Credentials: 36 | type: object 37 | properties: 38 | username: 39 | type: string 40 | default: admin@bookapi.com 41 | password: 42 | type: string 43 | default: password 44 | securitySchemes: 45 | bearerAuth: 46 | type: apiKey 47 | in: header 48 | name: Authorization # or another header name 49 | security: 50 | - bearerAuth: [] 51 | areas: # to filter documented areas 52 | path_patterns: 53 | - ^/api(?!/doc$) # Accepts routes under /api except /api/doc -------------------------------------------------------------------------------- /config/packages/nelmio_cors.yaml: -------------------------------------------------------------------------------- 1 | nelmio_cors: 2 | defaults: 3 | origin_regex: true 4 | allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] 5 | allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'] 6 | allow_headers: ['Content-Type', 'Authorization'] 7 | expose_headers: ['Link'] 8 | max_age: 3600 9 | paths: 10 | '^/': null 11 | -------------------------------------------------------------------------------- /config/packages/prod/jms_serializer.yaml: -------------------------------------------------------------------------------- 1 | jms_serializer: 2 | visitors: 3 | json_serialization: 4 | options: 5 | - JSON_UNESCAPED_SLASHES 6 | - JSON_PRESERVE_ZERO_FRACTION 7 | -------------------------------------------------------------------------------- /config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | utf8: true 4 | 5 | # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. 6 | # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands 7 | #default_uri: http://localhost 8 | 9 | when@prod: 10 | framework: 11 | router: 12 | strict_requirements: null 13 | -------------------------------------------------------------------------------- /config/packages/security.yaml: -------------------------------------------------------------------------------- 1 | security: 2 | enable_authenticator_manager: true 3 | # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords 4 | password_hashers: 5 | Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' 6 | App\Entity\User: 7 | algorithm: auto 8 | 9 | # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider 10 | providers: 11 | # used to reload user from session & other features (e.g. switch_user) 12 | app_user_provider: 13 | entity: 14 | class: App\Entity\User 15 | property: email 16 | firewalls: 17 | dev: 18 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 19 | security: false 20 | # main: 21 | # lazy: true 22 | # provider: app_user_provider 23 | 24 | login: 25 | pattern: ^/api/login 26 | stateless: true 27 | json_login: 28 | check_path: /api/login_check 29 | success_handler: lexik_jwt_authentication.handler.authentication_success 30 | failure_handler: lexik_jwt_authentication.handler.authentication_failure 31 | 32 | api: 33 | pattern: ^/api 34 | stateless: true 35 | jwt: ~ 36 | 37 | # activate different ways to authenticate 38 | # https://symfony.com/doc/current/security.html#the-firewall 39 | 40 | # https://symfony.com/doc/current/security/impersonating_user.html 41 | # switch_user: true 42 | 43 | # Easy way to control access for large sections of your site 44 | # Note: Only the *first* access control that matches will be used 45 | access_control: 46 | - { path: ^/api/login, roles: PUBLIC_ACCESS } 47 | - { path: ^/api/doc, roles: PUBLIC_ACCESS } 48 | - { path: ^/apip/, roles: IS_AUTHENTICATED_FULLY } 49 | - { path: ^/apip, roles: PUBLIC_ACCESS } 50 | - { path: ^/api, roles: IS_AUTHENTICATED_FULLY } 51 | 52 | when@test: 53 | security: 54 | password_hashers: 55 | # By default, password hashers are resource intensive and take time. This is 56 | # important to generate secure password hashes. In tests however, secure hashes 57 | # are not important, waste resources and increase test times. The following 58 | # reduces the work factor to the lowest possible values. 59 | Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 60 | algorithm: auto 61 | cost: 4 # Lowest possible value for bcrypt 62 | time_cost: 3 # Lowest possible value for argon 63 | memory_cost: 10 # Lowest possible value for argon 64 | -------------------------------------------------------------------------------- /config/packages/sensio_framework_extra.yaml: -------------------------------------------------------------------------------- 1 | sensio_framework_extra: 2 | router: 3 | annotations: false 4 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | default_path: '%kernel.project_dir%/templates' 3 | 4 | when@test: 5 | twig: 6 | strict_variables: true 7 | -------------------------------------------------------------------------------- /config/packages/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | email_validation_mode: html5 4 | 5 | # Enables validator auto-mapping support. 6 | # For instance, basic validation constraints will be inferred from Doctrine's metadata. 7 | #auto_mapping: 8 | # App\Entity\: [] 9 | 10 | when@test: 11 | framework: 12 | validation: 13 | not_compromised_password: false 14 | -------------------------------------------------------------------------------- /config/preload.php: -------------------------------------------------------------------------------- 1 | get('page', 1); 37 | $limit = $request->get('limit', 3); 38 | 39 | $idCache = "getAllAuthor-" . $page . "-" . $limit; 40 | 41 | $jsonAuthorList = $cache->get($idCache, function (ItemInterface $item) use ($authorRepository, $page, $limit, $serializer) { 42 | //echo ("L'ELEMENT N'EST PAS ENCORE EN CACHE !\n"); 43 | $item->tag("booksCache"); 44 | $authorList = $authorRepository->findAllWithPagination($page, $limit); 45 | $context = SerializationContext::create()->setGroups(["getAuthors"]); 46 | return $serializer->serialize($authorList, 'json', $context); 47 | }); 48 | 49 | return new JsonResponse($jsonAuthorList, Response::HTTP_OK, [], true); 50 | } 51 | 52 | /** 53 | * Cette méthode permet de récupérer un auteur en particulier en fonction de son id. 54 | * 55 | * @param Author $author 56 | * @param SerializerInterface $serializer 57 | * @return JsonResponse 58 | */ 59 | #[Route('/api/authors/{id}', name: 'detailAuthor', methods: ['GET'])] 60 | public function getDetailAuthor(Author $author, SerializerInterface $serializer): JsonResponse { 61 | $context = SerializationContext::create()->setGroups(["getAuthors"]); 62 | $jsonAuthor = $serializer->serialize($author, 'json', $context); 63 | return new JsonResponse($jsonAuthor, Response::HTTP_OK, [], true); 64 | } 65 | 66 | 67 | /** 68 | * Cette méthode supprime un auteur en fonction de son id. 69 | * En cascade, les livres associés aux auteurs seront aux aussi supprimés. 70 | * 71 | * /!\ Attention /!\ 72 | * pour éviter le problème : 73 | * "1451 Cannot delete or update a parent row: a foreign key constraint fails" 74 | * Il faut bien penser rajouter dans l'entité Book, au niveau de l'author : 75 | * #[ORM\JoinColumn(onDelete:"CASCADE")] 76 | * 77 | * Et resynchronizer la base de données pour appliquer ces modifications. 78 | * avec : php bin/console doctrine:schema:update --force 79 | * 80 | * @param Author $author 81 | * @param EntityManagerInterface $em 82 | * @return JsonResponse 83 | */ 84 | #[Route('/api/authors/{id}', name: 'deleteAuthor', methods: ['DELETE'])] 85 | #[IsGranted('ROLE_ADMIN', message: 'Vous n\'avez pas les droits suffisants pour supprimer un auteur')] 86 | public function deleteAuthor(Author $author, EntityManagerInterface $em, TagAwareCacheInterface $cache): JsonResponse { 87 | 88 | $em->remove($author); 89 | $em->flush(); 90 | 91 | // On vide le cache. 92 | $cache->invalidateTags(["booksCache"]); 93 | 94 | return new JsonResponse(null, Response::HTTP_NO_CONTENT); 95 | } 96 | 97 | /** 98 | * Cette méthode permet de créer un nouvel auteur. Elle ne permet pas 99 | * d'associer directement des livres à cet auteur. 100 | * Exemple de données : 101 | * { 102 | * "lastName": "Tolkien", 103 | * "firstName": "J.R.R" 104 | * } 105 | * 106 | * @param Request $request 107 | * @param SerializerInterface $serializer 108 | * @param EntityManagerInterface $em 109 | * @param UrlGeneratorInterface $urlGenerator 110 | * @return JsonResponse 111 | */ 112 | #[Route('/api/authors', name: 'createAuthor', methods: ['POST'])] 113 | #[IsGranted('ROLE_ADMIN', message: 'Vous n\'avez pas les droits suffisants pour créer un auteur')] 114 | public function createAuthor(Request $request, SerializerInterface $serializer, 115 | EntityManagerInterface $em, UrlGeneratorInterface $urlGenerator, ValidatorInterface $validator, 116 | TagAwareCacheInterface $cache): JsonResponse { 117 | $author = $serializer->deserialize($request->getContent(), Author::class, 'json'); 118 | 119 | // On vérifie les erreurs 120 | $errors = $validator->validate($author); 121 | if ($errors->count() > 0) { 122 | return new JsonResponse($serializer->serialize($errors, 'json'), JsonResponse::HTTP_BAD_REQUEST, [], true); 123 | } 124 | 125 | $em->persist($author); 126 | $em->flush(); 127 | 128 | // On vide le cache. 129 | $cache->invalidateTags(["booksCache"]); 130 | 131 | $context = SerializationContext::create()->setGroups(["getAuthors"]); 132 | $jsonAuthor = $serializer->serialize($author, 'json', $context); 133 | $location = $urlGenerator->generate('detailAuthor', ['id' => $author->getId()], UrlGeneratorInterface::ABSOLUTE_URL); 134 | return new JsonResponse($jsonAuthor, Response::HTTP_CREATED, ["Location" => $location], true); 135 | } 136 | 137 | 138 | /** 139 | * Cette méthode permet de mettre à jour un auteur. 140 | * Exemple de données : 141 | * { 142 | * "lastName": "Tolkien", 143 | * "firstName": "J.R.R" 144 | * } 145 | * 146 | * Cette méthode ne permet pas d'associer des livres et des auteurs. 147 | * 148 | * @param Request $request 149 | * @param SerializerInterface $serializer 150 | * @param Author $currentAuthor 151 | * @param EntityManagerInterface $em 152 | * @return JsonResponse 153 | */ 154 | #[Route('/api/authors/{id}', name:"updateAuthor", methods:['PUT'])] 155 | #[IsGranted('ROLE_ADMIN', message: 'Vous n\'avez pas les droits suffisants pour éditer un auteur')] 156 | public function updateAuthor(Request $request, SerializerInterface $serializer, 157 | Author $currentAuthor, EntityManagerInterface $em, ValidatorInterface $validator, 158 | TagAwareCacheInterface $cache): JsonResponse { 159 | 160 | // On vérifie les erreurs 161 | $errors = $validator->validate($currentAuthor); 162 | if ($errors->count() > 0) { 163 | return new JsonResponse($serializer->serialize($errors, 'json'), JsonResponse::HTTP_BAD_REQUEST, [], true); 164 | } 165 | 166 | $newAuthor = $serializer->deserialize($request->getContent(), Author::class, 'json'); 167 | $currentAuthor->setFirstName($newAuthor->getFirstName()); 168 | $currentAuthor->setLastName($newAuthor->getLastName()); 169 | 170 | $em->persist($currentAuthor); 171 | $em->flush(); 172 | 173 | // On vide le cache. 174 | $cache->invalidateTags(["booksCache"]); 175 | 176 | return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT); 177 | 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Controller/BookController.php: -------------------------------------------------------------------------------- 1 | get('page', 1); 63 | $limit = $request->get('limit', 3); 64 | 65 | $idCache = "getAllBooks-" . $page . "-" . $limit; 66 | 67 | $jsonBookList = $cache->get($idCache, 68 | function (ItemInterface $item) use ($bookRepository, $page, $limit, $serializer) { 69 | //echo ("L'ELEMENT N'EST PAS ENCORE EN CACHE !\n"); 70 | $item->tag("booksCache"); 71 | $bookList = $bookRepository->findAllWithPagination($page, $limit); 72 | $context = SerializationContext::create()->setGroups(["getBooks"]); 73 | return $serializer->serialize($bookList, 'json', $context); 74 | }); 75 | 76 | return new JsonResponse($jsonBookList, Response::HTTP_OK, [], true); 77 | } 78 | 79 | /** 80 | * Cette méthode permet de récupérer un livre en particulier en fonction de son id. 81 | * 82 | * @param Book $book 83 | * @param SerializerInterface $serializer 84 | * @return JsonResponse 85 | */ 86 | #[Route('/api/books/{id}', name: 'detailBook', methods: ['GET'])] 87 | public function getDetailBook(Book $book, SerializerInterface $serializer, VersioningService $versioningService): JsonResponse { 88 | $version = $versioningService->getVersion(); 89 | $context = SerializationContext::create()->setGroups(["getBooks"]); 90 | $context->setVersion($version); 91 | $jsonBook = $serializer->serialize($book, 'json', $context); 92 | return new JsonResponse($jsonBook, Response::HTTP_OK, [], true); 93 | } 94 | 95 | /** 96 | * Cette méthode permet de supprimer un livre par rapport à son id. 97 | * 98 | * @param Book $book 99 | * @param EntityManagerInterface $em 100 | * @return JsonResponse 101 | */ 102 | #[Route('/api/books/{id}', name: 'deleteBook', methods: ['DELETE'])] 103 | #[IsGranted('ROLE_ADMIN', message: 'Vous n\'avez pas les droits suffisants pour supprimer un livre')] 104 | public function deleteBook(Book $book, EntityManagerInterface $em, TagAwareCacheInterface $cache): JsonResponse { 105 | $em->remove($book); 106 | $em->flush(); 107 | // On vide le cache. 108 | $cache->invalidateTags(["booksCache"]); 109 | return new JsonResponse(null, Response::HTTP_NO_CONTENT); 110 | } 111 | 112 | /** 113 | * Cette méthode permet d'insérer un nouveau livre. 114 | * Exemple de données : 115 | * { 116 | * "title": "Le Seigneur des Anneaux", 117 | * "coverText": "C'est l'histoire d'un anneau unique", 118 | * "idAuthor": 5 119 | * } 120 | * 121 | * Le paramètre idAuthor est géré "à la main", pour créer l'association 122 | * entre un livre et un auteur. 123 | * S'il ne correspond pas à un auteur valide, alors le livre sera considéré comme sans auteur. 124 | * 125 | * @param Request $request 126 | * @param SerializerInterface $serializer 127 | * @param EntityManagerInterface $em 128 | * @param UrlGeneratorInterface $urlGenerator 129 | * @param AuthorRepository $authorRepository 130 | * @return JsonResponse 131 | */ 132 | #[Route('/api/books', name:"createBook", methods: ['POST'])] 133 | #[IsGranted('ROLE_ADMIN', message: 'Vous n\'avez pas les droits suffisants pour créer un livre')] 134 | public function createBook(Request $request, SerializerInterface $serializer, EntityManagerInterface $em, 135 | UrlGeneratorInterface $urlGenerator, AuthorRepository $authorRepository, ValidatorInterface $validator, 136 | TagAwareCacheInterface $cache): JsonResponse { 137 | 138 | $book = $serializer->deserialize($request->getContent(), Book::class, 'json'); 139 | 140 | // On vérifie les erreurs 141 | $errors = $validator->validate($book); 142 | if ($errors->count() > 0) { 143 | return new JsonResponse($serializer->serialize($errors, 'json'), JsonResponse::HTTP_BAD_REQUEST, [], true); 144 | //throw new HttpException(JsonResponse::HTTP_BAD_REQUEST, "La requête est invalide"); 145 | } 146 | 147 | $content = $request->toArray(); 148 | $idAuthor = $content['idAuthor'] ?? -1; 149 | $book->setAuthor($authorRepository->find($idAuthor)); 150 | 151 | $em->persist($book); 152 | $em->flush(); 153 | 154 | // On vide le cache. 155 | $cache->invalidateTags(["booksCache"]); 156 | 157 | $context = SerializationContext::create()->setGroups(["getBooks"]); 158 | $jsonBook = $serializer->serialize($book, 'json', $context); 159 | 160 | $location = $urlGenerator->generate('detailBook', ['id' => $book->getId()], UrlGeneratorInterface::ABSOLUTE_URL); 161 | 162 | return new JsonResponse($jsonBook, Response::HTTP_CREATED, ["Location" => $location], true); 163 | } 164 | 165 | 166 | /** 167 | * Cette méthode permet de mettre à jour un livre en fonction de son id. 168 | * 169 | * Exemple de données : 170 | * { 171 | * "title": "Le Seigneur des Anneaux", 172 | * "coverText": "C'est l'histoire d'un anneau unique", 173 | * "idAuthor": 5 174 | * } 175 | * 176 | * @param Request $request 177 | * @param SerializerInterface $serializer 178 | * @param Book $currentBook 179 | * @param EntityManagerInterface $em 180 | * @param AuthorRepository $authorRepository 181 | * @return JsonResponse 182 | */ 183 | #[Route('/api/books/{id}', name:"updateBook", methods:['PUT'])] 184 | #[IsGranted('ROLE_ADMIN', message: 'Vous n\'avez pas les droits suffisants pour éditer un livre')] 185 | public function updateBook(Request $request, SerializerInterface $serializer, 186 | Book $currentBook, EntityManagerInterface $em, AuthorRepository $authorRepository, 187 | ValidatorInterface $validator, TagAwareCacheInterface $cache): JsonResponse { 188 | 189 | $newBook = $serializer->deserialize($request->getContent(), Book::class, 'json'); 190 | 191 | $currentBook->setTitle($newBook->getTitle()); 192 | $currentBook->setCoverText($newBook->getCoverText()); 193 | 194 | // On vérifie les erreurs 195 | $errors = $validator->validate($currentBook); 196 | if ($errors->count() > 0) { 197 | return new JsonResponse($serializer->serialize($errors, 'json'), JsonResponse::HTTP_BAD_REQUEST, [], true); 198 | } 199 | 200 | $content = $request->toArray(); 201 | $idAuthor = $content['idAuthor'] ?? -1; 202 | 203 | $currentBook->setAuthor($authorRepository->find($idAuthor)); 204 | 205 | $em->persist($currentBook); 206 | $em->flush(); 207 | 208 | // On vide le cache. 209 | $cache->invalidateTags(["booksCache"]); 210 | 211 | return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT); 212 | } 213 | 214 | 215 | } 216 | -------------------------------------------------------------------------------- /src/Controller/ExternalApiController.php: -------------------------------------------------------------------------------- 1 | request( 27 | 'GET', 28 | 'https://api.github.com/repos/symfony/symfony-docs' 29 | ); 30 | 31 | return new JsonResponse($response->getContent(), $response->getStatusCode(), [], true); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/DataFixtures/AppFixtures.php: -------------------------------------------------------------------------------- 1 | userPasswordHasher = $userPasswordHasher; 19 | } 20 | 21 | public function load(ObjectManager $manager): void 22 | { 23 | // Création d'un user "normal" 24 | $user = new User(); 25 | $user->setEmail("user@bookapi.com"); 26 | $user->setRoles(["ROLE_USER"]); 27 | $user->setPassword($this->userPasswordHasher->hashPassword($user, "password")); 28 | $manager->persist($user); 29 | 30 | // Création d'un user admin 31 | $userAdmin = new User(); 32 | $userAdmin->setEmail("admin@bookapi.com"); 33 | $userAdmin->setRoles(["ROLE_ADMIN"]); 34 | $userAdmin->setPassword($this->userPasswordHasher->hashPassword($userAdmin, "password")); 35 | $manager->persist($userAdmin); 36 | 37 | // Création des auteurs. 38 | $listAuthor = []; 39 | for ($i = 0; $i < 10; $i++) { 40 | // Création de l'auteur lui même. 41 | $author = new Author(); 42 | $author->setFirstName("Prénom " . $i); 43 | $author->setLastName("Nom " . $i); 44 | $manager->persist($author); 45 | // On sauvegarde l'auteur créé dans un tableau. 46 | $listAuthor[] = $author; 47 | } 48 | 49 | for ($i=0; $i < 20; $i++) { 50 | $book = new Book(); 51 | $book->setTitle("Titre " . $i); 52 | $book->setCoverText("Quatrième de couverture numéro : " . $i); 53 | $book->setAuthor($listAuthor[array_rand($listAuthor)]); 54 | $book->setComment("Commentaire du bibliothécaire " . $i); 55 | $manager->persist($book); 56 | } 57 | 58 | $manager->flush(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Entity/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenClassrooms-Student-Center/7709361-API-REST-Symfony/993f38ed877ff55ad8c475667a8b07369a8d8946/src/Entity/.gitignore -------------------------------------------------------------------------------- /src/Entity/Author.php: -------------------------------------------------------------------------------- 1 | books = new ArrayCollection(); 69 | } 70 | 71 | public function getId(): ?int 72 | { 73 | return $this->id; 74 | } 75 | 76 | public function getLastName(): ?string 77 | { 78 | return $this->lastName; 79 | } 80 | 81 | public function setLastName(string $lastName): self 82 | { 83 | $this->lastName = $lastName; 84 | 85 | return $this; 86 | } 87 | 88 | public function getFirstName(): ?string 89 | { 90 | return $this->firstName; 91 | } 92 | 93 | public function setFirstName(?string $firstName): self 94 | { 95 | $this->firstName = $firstName; 96 | 97 | return $this; 98 | } 99 | 100 | /** 101 | * @return Collection 102 | */ 103 | public function getBooks(): Collection 104 | { 105 | return $this->books; 106 | } 107 | 108 | public function addBook(Book $book): self 109 | { 110 | if (!$this->books->contains($book)) { 111 | $this->books[] = $book; 112 | $book->setAuthor($this); 113 | } 114 | 115 | return $this; 116 | } 117 | 118 | public function removeBook(Book $book): self 119 | { 120 | if ($this->books->removeElement($book)) { 121 | // set the owning side to null (unless already changed) 122 | if ($book->getAuthor() === $this) { 123 | $book->setAuthor(null); 124 | } 125 | } 126 | 127 | return $this; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Entity/Book.php: -------------------------------------------------------------------------------- 1 | id; 74 | } 75 | 76 | public function getTitle(): ?string 77 | { 78 | return $this->title; 79 | } 80 | 81 | public function setTitle(string $title): self 82 | { 83 | $this->title = $title; 84 | 85 | return $this; 86 | } 87 | 88 | public function getCoverText(): ?string 89 | { 90 | return $this->coverText; 91 | } 92 | 93 | public function setCoverText(?string $coverText): self 94 | { 95 | $this->coverText = $coverText; 96 | 97 | return $this; 98 | } 99 | 100 | public function getAuthor(): ?Author 101 | { 102 | return $this->author; 103 | } 104 | 105 | public function setAuthor(?Author $author): self 106 | { 107 | $this->author = $author; 108 | 109 | return $this; 110 | } 111 | 112 | public function getComment(): ?string 113 | { 114 | return $this->comment; 115 | } 116 | 117 | public function setComment(?string $comment): self 118 | { 119 | $this->comment = $comment; 120 | 121 | return $this; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Entity/User.php: -------------------------------------------------------------------------------- 1 | id; 30 | } 31 | 32 | public function getEmail(): ?string 33 | { 34 | return $this->email; 35 | } 36 | 37 | public function setEmail(string $email): self 38 | { 39 | $this->email = $email; 40 | 41 | return $this; 42 | } 43 | 44 | /** 45 | * A visual identifier that represents this user. 46 | * 47 | * @see UserInterface 48 | */ 49 | public function getUserIdentifier(): string 50 | { 51 | return (string) $this->email; 52 | } 53 | 54 | /** 55 | * Méthode getUsername qui permet de retourner le champ qui est utilisé pour l'authentification. 56 | * 57 | * @return string 58 | */ 59 | public function getUsername(): string { 60 | return $this->getUserIdentifier(); 61 | } 62 | 63 | /** 64 | * @see UserInterface 65 | */ 66 | public function getRoles(): array 67 | { 68 | $roles = $this->roles; 69 | // guarantee every user at least has ROLE_USER 70 | $roles[] = 'ROLE_USER'; 71 | 72 | return array_unique($roles); 73 | } 74 | 75 | public function setRoles(array $roles): self 76 | { 77 | $this->roles = $roles; 78 | 79 | return $this; 80 | } 81 | 82 | /** 83 | * @see PasswordAuthenticatedUserInterface 84 | */ 85 | public function getPassword(): string 86 | { 87 | return $this->password; 88 | } 89 | 90 | public function setPassword(string $password): self 91 | { 92 | $this->password = $password; 93 | 94 | return $this; 95 | } 96 | 97 | /** 98 | * @see UserInterface 99 | */ 100 | public function eraseCredentials() 101 | { 102 | // If you store any temporary, sensitive data on the user, clear it here 103 | // $this->plainPassword = null; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/EventSubscriber/ExceptionSubscriber.php: -------------------------------------------------------------------------------- 1 | getThrowable(); 15 | 16 | if ($exception instanceof HttpException){ 17 | $data = [ 18 | 'status' => $exception->getStatusCode(), 19 | 'message' => $exception->getMessage() 20 | ]; 21 | $event->setResponse(new JsonResponse($data)); 22 | } else { 23 | $data = [ 24 | 'status' => 500, // Le status n'existe pas car ce n'est pas une exception HTTP, donc on met 500 par défaut. 25 | 'message' => $exception->getMessage() 26 | ]; 27 | $event->setResponse(new JsonResponse($data)); 28 | } 29 | } 30 | 31 | public static function getSubscribedEvents() 32 | { 33 | return [ 34 | 'kernel.exception' => 'onKernelException', 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Kernel.php: -------------------------------------------------------------------------------- 1 | decorated)($context); 21 | $schemas = $openApi->getComponents()->getSchemas(); 22 | 23 | $schemas['Token'] = new \ArrayObject([ 24 | 'type' => 'object', 25 | 'properties' => [ 26 | 'token' => [ 27 | 'type' => 'string', 28 | 'readOnly' => true, 29 | ], 30 | ], 31 | ]); 32 | $schemas['Credentials'] = new \ArrayObject([ 33 | 'type' => 'object', 34 | 'properties' => [ 35 | 'username' => [ 36 | 'type' => 'string', 37 | 'example' => 'admin@bookapi.com', 38 | ], 39 | 'password' => [ 40 | 'type' => 'string', 41 | 'example' => 'password', 42 | ], 43 | ], 44 | ]); 45 | 46 | $pathItem = new Model\PathItem( 47 | ref: 'JWT Token', 48 | post: new Model\Operation( 49 | operationId: 'postCredentialsItem', 50 | tags: ['Token'], 51 | responses: [ 52 | '200' => [ 53 | 'description' => 'Get JWT token', 54 | 'content' => [ 55 | 'application/json' => [ 56 | 'schema' => [ 57 | '$ref' => '#/components/schemas/Token', 58 | ], 59 | ], 60 | ], 61 | ], 62 | ], 63 | summary: 'Get JWT token to login.', 64 | requestBody: new Model\RequestBody( 65 | description: 'Generate new JWT Token', 66 | content: new \ArrayObject([ 67 | 'application/json' => [ 68 | 'schema' => [ 69 | '$ref' => '#/components/schemas/Credentials', 70 | ], 71 | ], 72 | ]), 73 | ), 74 | ), 75 | ); 76 | $openApi->getPaths()->addPath('/api/login_check', $pathItem); 77 | 78 | return $openApi; 79 | } 80 | } -------------------------------------------------------------------------------- /src/Repository/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenClassrooms-Student-Center/7709361-API-REST-Symfony/993f38ed877ff55ad8c475667a8b07369a8d8946/src/Repository/.gitignore -------------------------------------------------------------------------------- /src/Repository/AuthorRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('a') 31 | ->setFirstResult(($page - 1) * $limit) 32 | ->setMaxResults($limit); 33 | 34 | return $qb->getQuery()->getResult(); 35 | } 36 | 37 | // /** 38 | // * @return Author[] Returns an array of Author objects 39 | // */ 40 | /* 41 | public function findByExampleField($value) 42 | { 43 | return $this->createQueryBuilder('a') 44 | ->andWhere('a.exampleField = :val') 45 | ->setParameter('val', $value) 46 | ->orderBy('a.id', 'ASC') 47 | ->setMaxResults(10) 48 | ->getQuery() 49 | ->getResult() 50 | ; 51 | } 52 | */ 53 | 54 | /* 55 | public function findOneBySomeField($value): ?Author 56 | { 57 | return $this->createQueryBuilder('a') 58 | ->andWhere('a.exampleField = :val') 59 | ->setParameter('val', $value) 60 | ->getQuery() 61 | ->getOneOrNullResult() 62 | ; 63 | } 64 | */ 65 | } 66 | -------------------------------------------------------------------------------- /src/Repository/BookRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('b') 34 | ->setFirstResult(($page - 1) * $limit) 35 | ->setMaxResults($limit); 36 | 37 | return $qb->getQuery()->getResult(); 38 | } 39 | 40 | 41 | 42 | // /** 43 | // * @return Book[] Returns an array of Book objects 44 | // */ 45 | /* 46 | public function findByExampleField($value) 47 | { 48 | return $this->createQueryBuilder('b') 49 | ->andWhere('b.exampleField = :val') 50 | ->setParameter('val', $value) 51 | ->orderBy('b.id', 'ASC') 52 | ->setMaxResults(10) 53 | ->getQuery() 54 | ->getResult() 55 | ; 56 | } 57 | */ 58 | 59 | /* 60 | public function findOneBySomeField($value): ?Book 61 | { 62 | return $this->createQueryBuilder('b') 63 | ->andWhere('b.exampleField = :val') 64 | ->setParameter('val', $value) 65 | ->getQuery() 66 | ->getOneOrNullResult() 67 | ; 68 | } 69 | */ 70 | } 71 | -------------------------------------------------------------------------------- /src/Repository/UserRepository.php: -------------------------------------------------------------------------------- 1 | setPassword($newHashedPassword); 35 | $this->_em->persist($user); 36 | $this->_em->flush(); 37 | } 38 | 39 | // /** 40 | // * @return User[] Returns an array of User objects 41 | // */ 42 | /* 43 | public function findByExampleField($value) 44 | { 45 | return $this->createQueryBuilder('u') 46 | ->andWhere('u.exampleField = :val') 47 | ->setParameter('val', $value) 48 | ->orderBy('u.id', 'ASC') 49 | ->setMaxResults(10) 50 | ->getQuery() 51 | ->getResult() 52 | ; 53 | } 54 | */ 55 | 56 | /* 57 | public function findOneBySomeField($value): ?User 58 | { 59 | return $this->createQueryBuilder('u') 60 | ->andWhere('u.exampleField = :val') 61 | ->setParameter('val', $value) 62 | ->getQuery() 63 | ->getOneOrNullResult() 64 | ; 65 | } 66 | */ 67 | } 68 | -------------------------------------------------------------------------------- /src/Service/VersioningService.php: -------------------------------------------------------------------------------- 1 | requestStack = $requestStack; 23 | $this->defaultVersion = $params->get('default_api_version'); 24 | } 25 | 26 | /** 27 | * Récupération de la version qui a été envoyée dans le header "accept" de la requête HTTP 28 | * 29 | * @return string : le numéro de la version. Par défaut, la version retournée est celle définie dans le fichier de configuration services.yaml : "default_api_version" 30 | */ 31 | public function getVersion(): string 32 | { 33 | $version = $this->defaultVersion; 34 | 35 | $request = $this->requestStack->getCurrentRequest(); 36 | $accept = $request->headers->get('Accept'); 37 | // Récupération du numéro de version dans la chaîne de caractères du accept : 38 | // exemple "application/json; test=bidule; version=2.0" => 2.0 39 | $entete = explode(';', $accept); 40 | 41 | // On parcours toutes les entêtes pour trouver la version 42 | foreach ($entete as $value) { 43 | if (strpos($value, 'version') !== false) { 44 | $version = explode('=', $value); 45 | $version = $version[1]; 46 | break; 47 | } 48 | } 49 | return $version; 50 | } 51 | } -------------------------------------------------------------------------------- /symfony.lock: -------------------------------------------------------------------------------- 1 | { 2 | "api-platform/core": { 3 | "version": "2.6", 4 | "recipe": { 5 | "repo": "github.com/symfony/recipes", 6 | "branch": "master", 7 | "version": "2.5", 8 | "ref": "05b57782a78c21a664a42055dc11cf1954ca36bb" 9 | }, 10 | "files": [ 11 | "./config/packages/api_platform.yaml", 12 | "./config/routes/api_platform.yaml", 13 | "./src/Entity/.gitignore" 14 | ] 15 | }, 16 | "doctrine/annotations": { 17 | "version": "1.13", 18 | "recipe": { 19 | "repo": "github.com/symfony/recipes", 20 | "branch": "master", 21 | "version": "1.0", 22 | "ref": "a2759dd6123694c8d901d0ec80006e044c2e6457" 23 | }, 24 | "files": [ 25 | "./config/routes/annotations.yaml" 26 | ] 27 | }, 28 | "doctrine/cache": { 29 | "version": "2.1.1" 30 | }, 31 | "doctrine/collections": { 32 | "version": "1.6.8" 33 | }, 34 | "doctrine/common": { 35 | "version": "3.2.2" 36 | }, 37 | "doctrine/data-fixtures": { 38 | "version": "1.5.2" 39 | }, 40 | "doctrine/dbal": { 41 | "version": "3.3.2" 42 | }, 43 | "doctrine/deprecations": { 44 | "version": "v0.5.3" 45 | }, 46 | "doctrine/doctrine-bundle": { 47 | "version": "2.5", 48 | "recipe": { 49 | "repo": "github.com/symfony/recipes", 50 | "branch": "master", 51 | "version": "2.4", 52 | "ref": "ddddd8249dd55bbda16fa7a45bb7499ef6f8e90e" 53 | }, 54 | "files": [ 55 | "./config/packages/doctrine.yaml", 56 | "./src/Entity/.gitignore", 57 | "./src/Repository/.gitignore" 58 | ] 59 | }, 60 | "doctrine/doctrine-fixtures-bundle": { 61 | "version": "3.4", 62 | "recipe": { 63 | "repo": "github.com/symfony/recipes", 64 | "branch": "master", 65 | "version": "3.0", 66 | "ref": "1f5514cfa15b947298df4d771e694e578d4c204d" 67 | }, 68 | "files": [ 69 | "./src/DataFixtures/AppFixtures.php" 70 | ] 71 | }, 72 | "doctrine/doctrine-migrations-bundle": { 73 | "version": "3.2", 74 | "recipe": { 75 | "repo": "github.com/symfony/recipes", 76 | "branch": "master", 77 | "version": "3.1", 78 | "ref": "ee609429c9ee23e22d6fa5728211768f51ed2818" 79 | }, 80 | "files": [ 81 | "./config/packages/doctrine_migrations.yaml", 82 | "./migrations/.gitignore" 83 | ] 84 | }, 85 | "doctrine/event-manager": { 86 | "version": "1.1.1" 87 | }, 88 | "doctrine/inflector": { 89 | "version": "2.0.4" 90 | }, 91 | "doctrine/instantiator": { 92 | "version": "1.4.0" 93 | }, 94 | "doctrine/lexer": { 95 | "version": "1.2.2" 96 | }, 97 | "doctrine/migrations": { 98 | "version": "3.4.1" 99 | }, 100 | "doctrine/orm": { 101 | "version": "2.11.1" 102 | }, 103 | "doctrine/persistence": { 104 | "version": "2.3.0" 105 | }, 106 | "doctrine/sql-formatter": { 107 | "version": "1.1.2" 108 | }, 109 | "fig/link-util": { 110 | "version": "1.2.0" 111 | }, 112 | "friendsofphp/proxy-manager-lts": { 113 | "version": "v1.0.5" 114 | }, 115 | "jms/metadata": { 116 | "version": "2.6.1" 117 | }, 118 | "jms/serializer": { 119 | "version": "3.17.1" 120 | }, 121 | "jms/serializer-bundle": { 122 | "version": "4.0", 123 | "recipe": { 124 | "repo": "github.com/symfony/recipes-contrib", 125 | "branch": "master", 126 | "version": "3.0", 127 | "ref": "384cec52df45f3bfd46a09930d6960a58872b268" 128 | }, 129 | "files": [ 130 | "./config/packages/dev/jms_serializer.yaml", 131 | "./config/packages/jms_serializer.yaml", 132 | "./config/packages/prod/jms_serializer.yaml" 133 | ] 134 | }, 135 | "laminas/laminas-code": { 136 | "version": "4.5.1" 137 | }, 138 | "lcobucci/clock": { 139 | "version": "2.1.0" 140 | }, 141 | "lcobucci/jwt": { 142 | "version": "4.0.4" 143 | }, 144 | "lexik/jwt-authentication-bundle": { 145 | "version": "2.14", 146 | "recipe": { 147 | "repo": "github.com/symfony/recipes", 148 | "branch": "master", 149 | "version": "2.5", 150 | "ref": "5b2157bcd5778166a5696e42f552ad36529a07a6" 151 | }, 152 | "files": [ 153 | "./config/packages/lexik_jwt_authentication.yaml" 154 | ] 155 | }, 156 | "namshi/jose": { 157 | "version": "7.2.3" 158 | }, 159 | "nelmio/api-doc-bundle": { 160 | "version": "4.8", 161 | "recipe": { 162 | "repo": "github.com/symfony/recipes-contrib", 163 | "branch": "master", 164 | "version": "3.0", 165 | "ref": "c8e0c38e1a280ab9e37587a8fa32b251d5bc1c94" 166 | }, 167 | "files": [ 168 | "./config/packages/nelmio_api_doc.yaml", 169 | "./config/routes/nelmio_api_doc.yaml" 170 | ] 171 | }, 172 | "nelmio/cors-bundle": { 173 | "version": "2.2", 174 | "recipe": { 175 | "repo": "github.com/symfony/recipes", 176 | "branch": "master", 177 | "version": "1.5", 178 | "ref": "6bea22e6c564fba3a1391615cada1437d0bde39c" 179 | }, 180 | "files": [ 181 | "./config/packages/nelmio_cors.yaml" 182 | ] 183 | }, 184 | "nikic/php-parser": { 185 | "version": "v4.13.2" 186 | }, 187 | "phpdocumentor/reflection-common": { 188 | "version": "2.2.0" 189 | }, 190 | "phpdocumentor/reflection-docblock": { 191 | "version": "5.3.0" 192 | }, 193 | "phpdocumentor/type-resolver": { 194 | "version": "1.6.0" 195 | }, 196 | "phpstan/phpdoc-parser": { 197 | "version": "1.2.0" 198 | }, 199 | "psr/cache": { 200 | "version": "3.0.0" 201 | }, 202 | "psr/container": { 203 | "version": "2.0.2" 204 | }, 205 | "psr/event-dispatcher": { 206 | "version": "1.0.0" 207 | }, 208 | "psr/link": { 209 | "version": "2.0.1" 210 | }, 211 | "psr/log": { 212 | "version": "3.0.0" 213 | }, 214 | "sensio/framework-extra-bundle": { 215 | "version": "6.2", 216 | "recipe": { 217 | "repo": "github.com/symfony/recipes", 218 | "branch": "master", 219 | "version": "5.2", 220 | "ref": "fb7e19da7f013d0d422fa9bce16f5c510e27609b" 221 | }, 222 | "files": [ 223 | "./config/packages/sensio_framework_extra.yaml" 224 | ] 225 | }, 226 | "symfony/asset": { 227 | "version": "v6.0.3" 228 | }, 229 | "symfony/cache": { 230 | "version": "v6.0.3" 231 | }, 232 | "symfony/cache-contracts": { 233 | "version": "v3.0.0" 234 | }, 235 | "symfony/config": { 236 | "version": "v6.0.3" 237 | }, 238 | "symfony/console": { 239 | "version": "6.0", 240 | "recipe": { 241 | "repo": "github.com/symfony/recipes", 242 | "branch": "master", 243 | "version": "5.3", 244 | "ref": "da0c8be8157600ad34f10ff0c9cc91232522e047" 245 | }, 246 | "files": [ 247 | "./bin/console" 248 | ] 249 | }, 250 | "symfony/dependency-injection": { 251 | "version": "v6.0.3" 252 | }, 253 | "symfony/deprecation-contracts": { 254 | "version": "v3.0.0" 255 | }, 256 | "symfony/doctrine-bridge": { 257 | "version": "v6.0.3" 258 | }, 259 | "symfony/dotenv": { 260 | "version": "v6.0.3" 261 | }, 262 | "symfony/error-handler": { 263 | "version": "v6.0.3" 264 | }, 265 | "symfony/event-dispatcher": { 266 | "version": "v6.0.3" 267 | }, 268 | "symfony/event-dispatcher-contracts": { 269 | "version": "v3.0.0" 270 | }, 271 | "symfony/expression-language": { 272 | "version": "v6.0.3" 273 | }, 274 | "symfony/filesystem": { 275 | "version": "v6.0.3" 276 | }, 277 | "symfony/finder": { 278 | "version": "v6.0.3" 279 | }, 280 | "symfony/flex": { 281 | "version": "2.1", 282 | "recipe": { 283 | "repo": "github.com/symfony/recipes", 284 | "branch": "master", 285 | "version": "1.0", 286 | "ref": "c0eeb50665f0f77226616b6038a9b06c03752d8e" 287 | }, 288 | "files": [ 289 | "./.env" 290 | ] 291 | }, 292 | "symfony/framework-bundle": { 293 | "version": "6.0", 294 | "recipe": { 295 | "repo": "github.com/symfony/recipes", 296 | "branch": "master", 297 | "version": "5.4", 298 | "ref": "3cd216a4d007b78d8554d44a5b1c0a446dab24fb" 299 | }, 300 | "files": [ 301 | "./config/packages/cache.yaml", 302 | "./config/packages/framework.yaml", 303 | "./config/preload.php", 304 | "./config/routes/framework.yaml", 305 | "./config/services.yaml", 306 | "./public/index.php", 307 | "./src/Controller/.gitignore", 308 | "./src/Kernel.php" 309 | ] 310 | }, 311 | "symfony/http-client": { 312 | "version": "v6.0.5" 313 | }, 314 | "symfony/http-client-contracts": { 315 | "version": "v3.0.0" 316 | }, 317 | "symfony/http-foundation": { 318 | "version": "v6.0.3" 319 | }, 320 | "symfony/http-kernel": { 321 | "version": "v6.0.4" 322 | }, 323 | "symfony/maker-bundle": { 324 | "version": "1.36", 325 | "recipe": { 326 | "repo": "github.com/symfony/recipes", 327 | "branch": "master", 328 | "version": "1.0", 329 | "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" 330 | } 331 | }, 332 | "symfony/options-resolver": { 333 | "version": "v6.0.3" 334 | }, 335 | "symfony/password-hasher": { 336 | "version": "v6.0.3" 337 | }, 338 | "symfony/polyfill-intl-grapheme": { 339 | "version": "v1.24.0" 340 | }, 341 | "symfony/polyfill-intl-normalizer": { 342 | "version": "v1.24.0" 343 | }, 344 | "symfony/polyfill-mbstring": { 345 | "version": "v1.24.0" 346 | }, 347 | "symfony/polyfill-php56": { 348 | "version": "v1.20.0" 349 | }, 350 | "symfony/polyfill-php81": { 351 | "version": "v1.24.0" 352 | }, 353 | "symfony/property-access": { 354 | "version": "v6.0.3" 355 | }, 356 | "symfony/property-info": { 357 | "version": "v6.0.3" 358 | }, 359 | "symfony/proxy-manager-bridge": { 360 | "version": "v6.0.3" 361 | }, 362 | "symfony/routing": { 363 | "version": "6.0", 364 | "recipe": { 365 | "repo": "github.com/symfony/recipes", 366 | "branch": "master", 367 | "version": "6.0", 368 | "ref": "eb3b377a4dc07006c4bdb2c773652cc9434f5246" 369 | }, 370 | "files": [ 371 | "./config/packages/routing.yaml", 372 | "./config/routes.yaml" 373 | ] 374 | }, 375 | "symfony/runtime": { 376 | "version": "v6.0.3" 377 | }, 378 | "symfony/security-bundle": { 379 | "version": "6.0", 380 | "recipe": { 381 | "repo": "github.com/symfony/recipes", 382 | "branch": "master", 383 | "version": "5.3", 384 | "ref": "98f1f2b0d635908c2b40f3675da2d23b1a069d30" 385 | }, 386 | "files": [ 387 | "./config/packages/security.yaml" 388 | ] 389 | }, 390 | "symfony/security-core": { 391 | "version": "v6.0.5" 392 | }, 393 | "symfony/security-csrf": { 394 | "version": "v6.0.3" 395 | }, 396 | "symfony/security-http": { 397 | "version": "v6.0.5" 398 | }, 399 | "symfony/serializer": { 400 | "version": "v6.0.3" 401 | }, 402 | "symfony/service-contracts": { 403 | "version": "v3.0.0" 404 | }, 405 | "symfony/stopwatch": { 406 | "version": "v6.0.3" 407 | }, 408 | "symfony/string": { 409 | "version": "v6.0.3" 410 | }, 411 | "symfony/translation-contracts": { 412 | "version": "v3.0.0" 413 | }, 414 | "symfony/twig-bridge": { 415 | "version": "v6.0.5" 416 | }, 417 | "symfony/twig-bundle": { 418 | "version": "6.0", 419 | "recipe": { 420 | "repo": "github.com/symfony/recipes", 421 | "branch": "master", 422 | "version": "5.4", 423 | "ref": "bb2178c57eee79e6be0b297aa96fc0c0def81387" 424 | }, 425 | "files": [ 426 | "./config/packages/twig.yaml", 427 | "./templates/base.html.twig" 428 | ] 429 | }, 430 | "symfony/validator": { 431 | "version": "6.0", 432 | "recipe": { 433 | "repo": "github.com/symfony/recipes", 434 | "branch": "master", 435 | "version": "5.3", 436 | "ref": "c32cfd98f714894c4f128bb99aa2530c1227603c" 437 | }, 438 | "files": [ 439 | "./config/packages/validator.yaml" 440 | ] 441 | }, 442 | "symfony/var-dumper": { 443 | "version": "v6.0.3" 444 | }, 445 | "symfony/var-exporter": { 446 | "version": "v6.0.3" 447 | }, 448 | "symfony/web-link": { 449 | "version": "v6.0.3" 450 | }, 451 | "symfony/yaml": { 452 | "version": "v6.0.3" 453 | }, 454 | "twig/extra-bundle": { 455 | "version": "v3.3.8" 456 | }, 457 | "twig/twig": { 458 | "version": "v3.3.9" 459 | }, 460 | "webmozart/assert": { 461 | "version": "1.10.0" 462 | }, 463 | "willdurand/hateoas": { 464 | "version": "3.8.0" 465 | }, 466 | "willdurand/hateoas-bundle": { 467 | "version": "2.4", 468 | "recipe": { 469 | "repo": "github.com/symfony/recipes-contrib", 470 | "branch": "master", 471 | "version": "2.0", 472 | "ref": "34df072c6edaa61ae19afb2f3a239f272fecab87" 473 | }, 474 | "files": [ 475 | "./config/packages/bazinga_hateoas.yaml" 476 | ] 477 | }, 478 | "willdurand/negotiation": { 479 | "version": "3.1.0" 480 | }, 481 | "zircote/swagger-php": { 482 | "version": "4.2.13" 483 | } 484 | } 485 | -------------------------------------------------------------------------------- /templates/base.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Welcome!{% endblock %} 6 | 7 | {# Run `composer require symfony/webpack-encore-bundle` to start using Symfony UX #} 8 | {% block stylesheets %} 9 | {{ encore_entry_link_tags('app') }} 10 | {% endblock %} 11 | 12 | {% block javascripts %} 13 | {{ encore_entry_script_tags('app') }} 14 | {% endblock %} 15 | 16 | 17 | {% block body %}{% endblock %} 18 | 19 | 20 | --------------------------------------------------------------------------------