Skip to content

Instantly share code, notes, and snippets.

@mikamboo
Last active June 18, 2020 13:38
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mikamboo/be85f1c020bfc24e4d4e45e1afa83c68 to your computer and use it in GitHub Desktop.
Save mikamboo/be85f1c020bfc24e4d4e45e1afa83c68 to your computer and use it in GitHub Desktop.
Play with Symfony CRUD on Docker

Créer un projet Symfony 4 CRUD et REST api

Objectifs

Créer une app Symfony faisant du CRUD avec un dashboard admin et exposant une api RES. On va donc :

  • Créer un projet Symfony 4
  • Ajouter Sonata Admin bundle
  • Créer 2 entités : Company + User (1 relation many to 1 vers company)
  • Faire un CRUD User
  • Faire un CRUD Company
  • Exposer API User
  • Exposer API Company avec pagination

Péliminaires

  1. Quelques lectures sur l'env php, composer, symphony, doctrine ...
  2. Recherche d'une stack docker de dev pour éviter de poluer sa machine et être proche d'un déploiement réel
  3. Tester l'utilisation un simple conteneur docker PHP7 pour la creation du projet via composer

Creation du projet

J'utilise docker pour ne pas perdre de temps dans l'installation d'un environnement php/composer/symfony

Créons une image php7 + composer qui nous servira à ceer le projet et manipuler les dépendantes.

FROM php:7.2-cli

RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
RUN php composer-setup.php

WORKDIR /app
  • Build de l'image php_composer en local
docker build -t php_composer .
  • Création du projet en utilisant l'image php_composer
docker run -it --rm -v $(pwd):/app php_composer composer create-project symfony/skeleton symfony-app
  • Installation de dépendances : On lance un conteneur éphémère php_composer en ayant monté un volume pontant sur notre répertoire projet.
docker run -it --rm -v $(pwd)/symfony-app:/app php_composer sh 

composer require sonata-project/admin-bundle sonata-project/doctrine-orm-admin-bundle annotations migrations symfony/translation

composer require symfony/maker-bundle --dev
  • Configuration des paramètres d'accès à la base de données:

Il suffit de remplacer la ligne suivante (section DOCTRINE) dans le fichier .env à la racine du répertoire du projet symfony :

DATABASE_URL="mysql://symfony:symfony@db:3306/symfony"

Avec dans cette chaine de connexion :

  • symfony / symfony : user / password de la base de données mysql
  • symfony : nom de la base de donnée
  • db : le database host

Environnement de dev sous Docker

Pour développer et exécuter le code de notre projet on va utiliser encore un fois docker mais avec une stack un peu plus élaborée.

Après une rapide recherche (mots clé docker+symfony), la stack docker-compose docker-symfony de @eko sur Github semble répondre au besoin (DANGER: Ne jamais utiliser des images docker de sources non maîtrisées en PROD).

Elle se compose de 4 services (conteneurs quiseront lancés cf docker.compose.yaml) :

  • db : A partir de l'image officielle mysql pour la base de donnée. ATTENTION : db est aussi le nom hôte que nous avons rensigné dans la configuration de notre projet symfony.
  • php : A partir de l'image custom définie dans le répertoire ./php-fpm. C'est l'environnement d'exécution de notre projet, le code est monté dans ce contener via un volume.
  • nginx : A partir de l'image custom définie dans le répertoire ./nginx. C'est le web server qui va exposer notre application.
  • elk : Exécute une stack ELK créée à partir de l'image willdurand/elk pour collecter et visualiser les logs.
  • Récupération de la stack docker : elle contient des définitions pour les images bdd mysql, php, nginx, elk.

On clone cette stack et on y place notre répertoire projet symfony-app que l'on renomme en symfony. Selon la configuration de docker sur le poste il est possible que ledossier projet symfony-app soit la propriété du root user. Il sera nécessaire de changer les doits du dossier projet symfony-app avec un sudo chown <username> symfony-app

git clone https://github.com/eko/docker-symfony.git symfony-docker-dev
mv ./symfony-app ./symfony-docker-dev/symfony

Voici l'abrorescence de notre stack docker-symfony à ce stade:

├── docker-compose.travis.yml
├── docker-compose.yml
├── elk/
├── LICENSE.md
├── logs/
├── nginx/
├── php-fpm/
├── README.md
└── symfony/

C'est le moment d'ouvrir un IDE php avec comme cible le dossier symfony-docker-dev/symfony.

  • Mise à jour de la configution locale : symfony.localhost sera ajouté comme url de notre projet symfony conformémet aux instructions de docker-symfony.
echo "127.0.0.1       symfony.localhost" >> /etc/hosts
sudo service networking restart
  • Démarrer la stack docker

Depuis la racine du dossier symfony-docker-dev on éxécute :

docker-compose up

Dans la suite ce ce billet, les commandes commençant pas php ... seront exécutés dans le conteneur php (pour rappel notre environnement de dev est sous docker). Ainsi on peut par exemple ouvrir un terminal sur le conteneur php via la commande docker exec -it <nom_conteneur_php> /bin/sh et ensuite exécuter les commande que l'on souhaite.

Création des entités CRUD

Dans le dossier src du projet on a le répertoire Entity qui sert à stocker les entités pour l'ORM.

On va utiliser le générateur Symfony MakerBundle installé plus haut via composer.

  • Exécuter pour chaque entité la commande
php bin/console make:entity
php bin/console make:crud

La commande est interractive et permet de génerer une entité pour l'ORM Doctrine

  • Exemple entité créée : src/Entity/Company.php
<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * @ORM\Entity
 * @ORM\Table(name="company")
 */
class Company
{
  /**
   * @ORM\Column(type="integer")
   * @ORM\Id
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  private $id;

  /**
   * @var string
   *
   * @ORM\Column(name="name", type="string")
   */
  private $name;

  //... suite de la classe
}
  • Mise à jour de la base

Une fois les modèles créé on udate la base (Atention en DEV uniquement utiliser l'incremental update sinon)

php bin/console doctrine:schema:update --force

Les config sonata admin

Pour ce la on va utiliser la commande

php bin/console make:sonata:admin

Les controlleurs CRUD

Optionels : Sauf nécessité de surcharge.

Création de l'API REST

Pour créer notre API on va utiliser JMSSerializerBundle et FOSRestBundle.

On commence par rajouter ces dépendances.

composer require symfony/serializer
composer require friendsofsymfony/rest-bundle

Configurations

  • FOSRestBundle : packages/fos_rest.yaml
fos_rest: 
  view:
    view_response_listener:  true
  format_listener:
    rules:
      - { path: ^/api, prefer_extension: true, fallback_format: json, priorities: [ json, html ] }
      - { path: '^/', priorities: [ 'text/html', '*/*'], fallback_format: html, prefer_extension: true }
  • Annotations : config/routes/anotations.yaml
controllers:
  resource: ../../src/Controller/
  type: annotation

rest_controller:
  resource: ../../src/ControllerRest/
  type: annotation
  prefix: /api

Tester l'API

  • Avec un client http ex. Postman ou cURL
curl -X GET \
  http://symfony.localhost/api/users \
  -H 'cache-control: no-cache' \
  -H 'content-type: application/json' \

Réponse

[
    {
        "id": 1,
        "firstname": "Mike",
        "lastname": "PAMBO",
        "company": {
            "id": 2,
            "name": "Symfony",
            "city": "Paris",
        }
    }
    
    //...

]

Ressources utiles

@mikamboo
Copy link
Author

mikamboo commented Aug 2, 2019

image
image

@mikamboo
Copy link
Author

mikamboo commented Aug 3, 2019

Mises en garde

  • Attention aux images Docker récupérées en ligne ne jamais intégerer pour la production une image dpnt on ne maitrise pas la source.

Commands

php bin/console doctrine:database:create #if necessary
php bin/console doctrine:schema:create #if necessary

php bin/console make:crud
php bin/console make:controller
php bin/console make:sonata:admin

php bin/console doctrine:schema:validate
php bin/console doctrine:schema:update --force

@mikamboo
Copy link
Author

mikamboo commented Aug 3, 2019

php bin/console make:sonata:admin

Welcome to the Sonata Admin
---------------------------

 The fully qualified model class:
 > App\Entity\User

 The admin class basename [UserAdmin]:
 > 

 Do you want to generate a controller? (yes/no) [no]:
 > yes

 The controller class basename [UserAdminController]:
 > 

 Do you want to update the services YAML configuration file? (yes/no) [yes]:
 > 

 The services YAML configuration file [services.yaml]:
 > 

 The admin service ID [admin.user]:

@mikamboo
Copy link
Author

mikamboo commented Aug 3, 2019

Quelques erreurs rencontrées

Message : Unable to write in the logs directory (/var/www/symfony/var/log)

Source : Problème de droits sur le répertoires var/log ou var/cache créés par l'application symphony.

Solution : Modifier les droits sur le répertoire var dans le conteneur php. On peut le faire en faisant un
docker exec chmod -R 777 var/cache var/logs.


Message : An exception has been thrown during the rendering of a template ("You must define an associated_property option or create a App\Entity\Company::__toString method to the field option company from service admin.user is ")

Source : Problème de configuration de la relation User/Company dans Sonata admin

Solution 1 : Gérer la configuration de CompanyAdmin pour tenir compte de la relation

Solution 2 : Rajouter une méthode __toString dans le modèle Company.


Message : An exception has been thrown during the rendering of a template ("Unable to generate a URL for the named route "admin_app_company_list" as such route does not exist."

Solution : Vérifier la configuration déclartive dans services.yaml et vider le cache -> bin/console cache:clear.


Message : Symfony API with FOSRestBundle : circular reference has been detected
Solution : Mettre en place une CircularHandlerFactory

@mikamboo
Copy link
Author

mikamboo commented Aug 3, 2019

Exemple controlleur REST

<?php

namespace App\ControllerRest;

use App\Entity\User;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\View\View;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Doctrine\Common\Persistence\ObjectRepository;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityNotFoundException;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;


/**
 * Class UserController
 * @package App\ControllerRest
 * 
 * //TODO : Créer une classe service et dao
 * //TODO : Gestion des erreurs dans la couche service 
 * //TODO : Valider le contenur des Request
 * //TODO : Céer les test unitaires de chaque action
 * 
 */
final class UserController extends FOSRestController
{
    /**
     * @var ObjectRepository
     */
    private $userRepository; //TODO : Move to data access layer

    /**
     * UserRepository.
     * @param EntityManagerInterface $entityManager
     */
    private $entityManager; //TODO : Move to data access layer

    /**
     * UserController constructor.
     */
    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
        $this->userRepository = $entityManager->getRepository(User::class);
    }

    /**
     * Retrieves a collection of User resource
     * @Rest\Get("/users")
     * 
     * @return View
     */
    public function findAll()
    {
        $users = $this->userRepository->findAll();

        return View::create($users, Response::HTTP_OK);
    }

    /**
     * Save User 
     */
    private function saveUser(User $user)
    {
        $this->entityManager->persist($user);
        $this->entityManager->flush();
    }

    /**
     * Retrieves single User resource
     * @Rest\Get("/users/{userId}")
     * 
     * @return View
     */
    public function findOne(int $userId): View
    {
        $user = $this->userRepository->findOneBy(['id' => $userId]);

        if (!$user) {
            throw new EntityNotFoundException('User with id '.$user.' does not exist!');
        }

        return View::create($user, Response::HTTP_OK);
    }

    /**
     * Creates an User resource
     * @Rest\Post("/users")
     * @param Request $request
     * @return View
     */
    public function createUser(Request $request): View
    {
        $user = new User();
        $user->setFirstName($request->get('firstname'));
        $user->setLastName($request->get('lastname'));

        $this->saveUser($user);

        return View::create($user, Response::HTTP_CREATED);
    }

    /**
     * Update an User resource
     * @Rest\Put("/users/{userId}")
     * @ParamConverter("userData", converter="fos_rest.request_body")
     * @param Request $request
     * @return View
     */
    public function updateUser(int $userId, Request $request, User $userData): View
    {
        $user = $this->userRepository->findOneBy(['id' => $userId]);

        if (!$user) {
            throw new EntityNotFoundException('User with id '.$userId.' does not exist!');
        }

        $user->setFirstName($request->get('firstname'));
        $user->setLastName($request->get('lastname'));

        $this->saveUser($user);

        return View::create($user, Response::HTTP_OK);
    }

        /**
     * Update an User resource
     * @Rest\Delete("/users/{userId}")
     * @return View
     */
    public function deleteUser(int $userId): View
    {
        $user = $this->userRepository->findOneBy(['id' => $userId]);

        if (!$user) {
            throw new EntityNotFoundException('User with id '.$userId.' does not exist!');
        }
        $this->entityManager->remove($user);
        $this->entityManager->flush();

        return View::create([], Response::HTTP_NO_CONTENT);
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment