Création d'un projet sous Symfony 4 avec Vagrant

php symfony vagrant

Symfony est le framework PHP le plus populaire en France. La version 4 est sortie en décembre et se veut plus légère et moins complexe que la précédente version. Voyons donc comment comment l’installer avec une machine virtuelle qui fera tourner un serveur Apache.

Vagrant

Installation

Vagrant est un outil qui permet de configurer et ainsi de reproduire facilement des machines virtuelles. L’environnement est exactement le même pour tous les développeurs, quel que soit le système d’exploitation de la machine hôte.

Vagrant utilise Virtual Box. Il faut donc commencer par installer les deux outils via leurs sites officiels. Voici un exemple pour l’OS Ubuntu 18.04.

  • installation de Virtual Box
    $ wget https://download.virtualbox.org/virtualbox/5.2.12/virtualbox-5.2_5.2.12-122591~Ubuntu~bionic_amd64.deb
    $ dpkg -i virtualbox-5.2_5.2.12-122591~Ubuntu~bionic_amd64.deb
    
  • installation de Vagrant
    $ wget https://releases.hashicorp.com/vagrant/2.1.1/vagrant_2.1.1_x86_64.deb
    $ dpkg -i vagrant_2.1.1_x86_64.deb
    

Une fois Vagrant & Virtual Box installés, on peut initialiser un nouveau projet avec la commande vagrant

$ mkdir RentMyRoom
$ cd RentMyRoom
$ vagrant init

Configuration

vagrant init a créé un nouveau fichier Vagrantfile qui contient toute la configuration de notre machine virtuelle. La directive

On met à jour la configuration de Vagrant pour notre projet

# Vagrantfile
Vagrant.configure("2") do |config|
  # charge une machine virtuelle pré-configurée téléchargeable sur https://app.vagrantup.com/boxes/search
  # cette VM contient Ubuntu 18.04, PHP 7.2, MYSQL 5.7, memcached, Composer
  config.vm.box = "secalith/bionic64"
  # configure le réseaux
  config.vm.network "private_network", ip: "192.168.33.10"
  # on utilise un sous-dossier contiendra notre projet
  config.vm.synced_folder "./RentMyRoom/", "/var/www/html", owner: "www-data", group: "www-data", id: 'source'
  # on crée écrase la configuration par défault d'Apache
  config.vm.synced_folder "./apache2/", "/etc/apache2/sites-enabled", owner: "root", group: "root", id: 'vhost'
  # on force le redémarrage une fois la configuration d'Apache écrasée 
  config.vm.provision :shell, run: "always", inline: "/etc/init.d/apache2 restart"
end

et notre Vhost Apache spécifique

# apache2/000-default.conf

<VirtualHost *:80>
    ServerName rent-my-room.local
    ServerAlias www.rent-my-room.local

    DocumentRoot /var/www/html/public
    <Directory /var/www/html/public>
        AllowOverride None
        Order Allow,Deny
        Allow from All

        <IfModule mod_rewrite.c>
            Options -MultiViews
            RewriteEngine On
            RewriteCond %{REQUEST_FILENAME} !-f
            RewriteRule ^(.*)$ index.php [QSA,L]
        </IfModule>
    </Directory>

    <Directory /var/www/project/public/bundles>
        <IfModule mod_rewrite.c>
            RewriteEngine Off
        </IfModule>
    </Directory>
    ErrorLog /var/log/apache2/project_error.log
    CustomLog /var/log/apache2/project_access.log combined

</VirtualHost>

et on démarre notre machine virtuelle

$ vagrant up

Cette commande va télécharger notre machine virtuelle et la démarrer.

Symfony

On initialise le projet avec composer. Pour cela, on se connecte à la machine virtuelle avec vagrant ssh

$ vagrant ssh
$ cd html

et on installe les dépendances

$ composer install

Il suffit ensuite de se rendre à l’adresse 192.168.33.10 et la page d’accueil s’affiche!!

Prise en main de Symfony

Imaginons une application web permettant de louer des salles Room à des utilisateurs User.

Gestion des utilisateurs

Symfony utilise des bundle qui sont des “morceaux d’applications” utilisables dans d’autres projets. Dans notre cas, au lieu de partir de zéro, nous utiliserons FOSUserBundle. Ce Bundle gère

  • la création d’utilisateur
  • la confirmation de son adresse mail
  • la connexion de l’utilisateur

Pour l’installer il suffit de suivre les indications de la documentation officielle.

Création de notre entité

Création du modèle

Nous allons ici créer notre modèle Room et créer les actions CRUD. Dans un premier temps, il faut créer notre entité. Pour cela on utilise la commande doctrine:generate:entities

$ php bin/console doctrine:generate:entities Room

Cette commande va créer:

  • RentMyRoom/src/Entity/Room.php
  • RentMyRoom/src/Repository/RoomRepository.php

Pour mettre à jour notre base de données, deux options existent:

  • Mettre à jour la base de donnée automatiquement
    $ php bin/console doctrine:schema:update
    
  • Créer une migration contenant les différences depuis la dernière migration
    $ php bin/console doctrine:migration:diff
    

    Et pour lancer toutes les migrations non-jouées, on utilise

    $ php bin/console doctrine:migration:migrate
    

Création de tout le reste

Il suffit ensuite d’utiliser make:crud afin de générer automatiquement le controller, le formulaire et les vues en fonction de notre modèle:

$ php bin/console make:crud Room

Cette commande va créer ces fichiers

  • src/Controller/RoomController.php contenant le controller et toutes les actions CRUD
  • src/Form/RoomType.php contenant le formulaire
  • templates/room/ contenant les vues CRUD

Utiliser Bootstrap pour le formulaire

Il suffit d’éditer le formulaire contenu dans RentMyRoom/src/Form/RoomType.php. La fonction FormBuilderInterface::add permet de spécifier le type du champ et de spécifier la classe CSS.

// RentMyRoom/src/Form/RoomType.php
<?php

namespace App\Form;

// uses ..

class RoomType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('address', TextType::class, ['attr'   => ['class' => 'form-control']])
            ->add('start_at', DateTimeType::class, ['widget' => 'single_text', 'attr'   => ['class' => 'form-control']])
            ->add('end_at', DateTimeType::class, ['widget' => 'single_text', 'attr'   => ['class' => 'form-control']])
            ->add('price', NumberType::class, ['attr'   => ['class' => 'form-control']])
            ->add('size', NumberType::class, ['attr'   => ['class' => 'form-control']])
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(['data_class' => Room::class,]);
    }
}

Ne pas oublier d’ajouter Bootstrap au fichier Template/base.html.twig

Créer la relation User has many Rooms

Il suffit de se rendre dans notre entité Room et d’ajouter la liaison

<?php
// Entity/Room.php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\RoomRepository")
 */
class Room
{
    // ...

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\User")
     */
    private $user;

    public function getUser() : User
    {
        return $this->user;
    }

    public function setUser(User $user): void
    {
        $this->user = $user;
    }
}

On génère ensuite la migration

$ php bin/console doctrine:migrations:diff

Cette commande va nous générer automatiquement un migration qui va ajouter

  • la colonne user_id dans la table room
  • la clé primaire afin de vérifier l’existence de l’user
<?php 
// src/Migrations/Version20180619075756.php

// ..

public function up(Schema $schema) : void
{
    // this up() migration is auto-generated, please modify it to your needs
    $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');

    $this->addSql('ALTER TABLE room ADD user_id INT DEFAULT NULL');
    $this->addSql('ALTER TABLE room ADD CONSTRAINT FK_729F519BA76ED395 FOREIGN KEY (user_id) REFERENCES users (id)');
    $this->addSql('CREATE INDEX IDX_729F519BA76ED395 ON room (user_id)');
}

Pour modifier notre base de données, il faut lancer la commande

$ php bin/console doctrine:migrations:migrate

Modifier le controller

On modifie ensuite notre controller afin d’ajouter l’user connecté à la room créée

<?php
// src/Controller/RoomController.php

/**
 * @Route("/room")
 */
class RoomController extends Controller
{
    // ..

    /**
     * @Route("/new", name="room_new", methods="GET|POST")
     */
    public function new(Request $request): Response
    {
        $room = new Room();
        $form = $this->createForm(RoomType::class, $room);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $em = $this->getDoctrine()->getManager();
            $room->setUser($this->getUser());
            $em->persist($room);
            $em->flush();

            return $this->redirectToRoute('room_index');
        }

        return $this->render('room/new.html.twig', [
            'room' => $room,
            'form' => $form->createView(),
        ]);
    }

    // ...

}

Afin de restreindre les actions edit et destroy à l’utilisateur qui possède

<?php
// src/Controller/RoomController.php

/**
 * @Route("/room")
 */
class RoomController extends Controller
{

    // ...

    /**
     * @Route("/{id}/edit", name="room_edit", methods="GET|POST")
     */
    public function edit(Request $request, Room $room): Response
    {
        $this->checkRoomOwner($room);
        // ...
    }

    /**
     * @Route("/{id}", name="room_delete", methods="DELETE")
     */
    public function delete(Request $request, Room $room): Response
    {
        $this->checkRoomOwner($room);
        // ...
    }


    private function checkRoomOwner(Room $room)
    {
        $currentUser = $this->getUser();
        if(is_null($currentUser)) {
            // should not happens because of security bundle
            throw $this->createAccessDeniedException('You must be connected to access on this pas');
        }elseif($currentUser != $room->getUser()){
            // user should own this room
            throw $this->createAccessDeniedException('You do not own this room');
        }
    }
}