Vous avez déjà travaillé avec de vieux projet PHP (3,4), du “include-ception” ou tout simplement un framework non PSR-0? Voici un retour sur les étapes employé dans différent cas de migration de “legacy” vers Symfony 2.
1. DE LEGACY À SYMFONY
SYMFONY MONTRÉAL
16 JUIN 2016
Etienne Lachance
@elachance
2. QUI SUIS-JE
Sys admin de formation
Programmeur depuis ~10 ans
Propriétaire de elcweb.ca
Consultation en entreprise
Programmation
Hébergement spécialisé
Fin de l'auto promotion
3. CONTEXT
Evolution d'un projet "Legacy" sans affecter la productivité mais en
introduisant les bonne pratique d’un nouveau framework.
4. PAR LEGACY J'ENTEND
peu/pas de documentation
peu/pas de tests
Code procédural
Code spaghetti
Duplication de code (copier-coller)
Include-ception
Couplage de responsabilité
... Non SOLID
8. 1. PARALLEL
simple a implémenter (mod_rewrite)
aucune communication direct entre les 2 applications
utilisation de la BD ou Redis pour l'échange entre les 2 apps
peu/pas d'impacte sur l'application 1
9. 2. PROXY
l'utilisateur voie uniquement une application (Symfony)
necessite plus de travail pour la mise en place
"wrapper" pour les requêtes a l'application Legacy
authenti cation
sécurité entre les 2 applications
peu/pas d'impacte sur l'application Legacy
10. 3. INTÉGRATION
On veut changer la structure fondamental du code actuel.
une seule application
OPTION PRÉSENTÉ
11. QU'EST-CE QUE SYMFONY?
Symfony est *
une collection de composante
un framework applicatif
une philosophy
une communauté
Symfony est a la base un framework HTTP
GESTION DES REQUÊTE / RÉPONSE
source: http://symfony.com/what-is-symfony
16. EXEMPLE DE TYPE "INCLUDE-CEPTION"
<?php
// index.php
include("includes/common.php");
include("includes/config.php");
$mod = $_GET['mod'];
if ($mod=="" || !preg_match('/^[A-Za-z1-90_]+$/Ui',$mod))
$mod = "dashboard";
include ("modules/".$mod.".php");
aucun namespace
logique basé sur include() / require()
17. LEGACY CONTROLLER
namespace AppBundleController;
use ...;
class LegacyController extends Controller
{
/** @Route("/index.php") */
public function legacyAction()
{
// __DIR__ == 'src/AppBundle/Controller'
include __DIR__ . '/../includes/common.php';
include __DIR__ . '/../includes/config.php';
// @todo: renommé $mod pour $module
$mod = $_GET['mod'];
if ($mod=="" || !preg_match('/^[A-Za-z1-90_]+$/Ui', $mod)) {
$mod = "dashboard";
}
18. LEGACY CONTROLLER - SUITE
// Include module file
$filename = __DIR__ . '/../modules/' . $mod . '.php';
if (!file_exists($filename)) {
throw new NotFoundHttpException('Module ' . $mod . ' not found');
}
// ob_start: Turn on output buffering
ob_start();
include $filename;
return new Response(ob_get_clean());
}
}
19. RÉCAPITULATION
déplacer les chiers a l'extérieur du répertoire publique
véri er si le module exist
si le module n'existe pas, retourne une erreur 404
encapsuler les "echo" du code legacy dans un objet Response
24. PROVIDER
Est responsable d'aller chercher l'utilisateur
Implement UserProviderInterface
namespace AppBundleSecurityUser;
use ...
class UserProvider implements UserProviderInterface
{
public function loadUserByUsername($username) { ... }
public function refreshUser(UserInterface $user) { ... }
public function supportsClass($class)
{
return $class === 'AppBundleSecurityUserUser';
}
}
25. ENCODER
Responsable d'encoder et de valider un mot de passe
namespace AppBundleSecurityEncoder;
use SymfonyComponentSecurityCoreEncoderBasePasswordEncoder;
class LegacyMd5Encoder extends BasePasswordEncoder
{
public function isPasswordValid($encoded, $raw, $salt = null) :bool
{
return $this->comparePasswords(strtolower($encoded), strtolower($this->encodePassword($raw, $
}
public function encodePassword($raw, $salt = null) :string
{
return md5($raw . 'secret_global_a_l_application');
}
}
29. DOCTRINE / REPOSITORY
Dans le contexte de Doctrine, un Repository est utilisé pour allez chercher
l'information
Centraliser les requêtes SQL
Isoler les requêtes du "controlleur"
Classi er par context d'objet (Utilisateur/Produit/Client)
30. LEGACY
<?php
// ...
$conn = mysql_connect($db_host, $db_user, $db_password);
if (!$conn) {
echo "Unable to connect to DB: " . mysql_error();
exit;
}
if (!mysql_select_db($dbname)) {
echo "Unable to select mydbname: " . mysql_error();
exit;
}
$sql = "SELECT * FROM products WHERE category = ".mysql_real_escape_string($_GET['cat']).";"
31. LEGACY - SUITE
$result = mysql_query($sql);
if (!$result) {
echo "Could not successfully run query ($sql) from DB: " . mysql_error();
exit;
}
if (mysql_num_rows($result) == 0) {
echo "No rows found, nothing to print so am exiting";
exit;
}
$data = array();
while ($row = mysql_fetch_assoc($result)) {
$data = $row;
}
mysql_free_result($result);
32. GÉNÉRATION D'ENTITÉ A PARTIR D'UNE BASE DE DONNÉ
EXISTANTE
$ php bin/console doctrine:mapping:import --force AcmeBlogBundle xml
$ php bin/console doctrine:mapping:convert annotation ./src
$ php bin/console doctrine:generate:entities AcmeBlogBundle
http://symfony.com/doc/current/cookbook/doctrine/reverse_engineering.html
33. REPOSITORY
namespace AppBundleEntity;
use DoctrineORMEntityRepository;
class ProductRepository extends EntityRepository
{
public function findByCategory($categoryId)
{
$sql = "SELECT * FROM products WHERE category = ".mysql_real_escape_string($categoryId
$stmt = $this->getEntityManager()->getConnection()->prepare($sql);
$stmt->execute();
return $stmt->findAll();
}
}
RawSQLTrait: https://gist.github.com/estheban/3eae41271f6cf5f3180a
34. UTILISATION DANS UN CONTROLLEUR
class ProductController extends Controller
{
/**
* @Route("/product.php/category/{id}")
*/
public function productByCategory(Category $category)
{
// throw 404 si pas de Catégorie trouvée
$entityManager = $this->getDoctrine()->getManager();
return $entityManager
->getRepository("AppBundle:Product")
->findByCategory($category->getId());
}
}
Voir: @ParamConverter
35. TEMPLATES
Ne pas convertir les templates pour le "Fun"
On peut retourner:
une réponse directement (aucun template)
un template en php
un template en twig (natif)
un template en smarty
un template en ...
36. EXEMPLE D'UN CONTROLLEUR QUI RETOURNE UNE RÉPONSE
BASÉ SUR UN TEMPLATE
public function indexAction()
{
// some logic to retrieve the blogs
$blogs = ...;
$this->render(
'AcmeBlogBundle:Blog:index.html.twig',
array('blogs' => $blogs)
);
}
37. EXEMPLE D'UN TEMPLATE EN TWIG
{# app/Resources/views/blog/index.html.twig #}
{% extends 'blog/layout.html.twig' %}
{% block content %}
{% for blogPost in blogs %}
<h2>{{ blogPost.title }}</h2>
<p>{{ blogPost.body }}</p>
{% endfor %}
{% endblock %}
38. EXEMPLE DE SMARTY ET TWIG
protected function renderSmarty($template, $outputData)
{
$smarty = new SmartyEngine();
$smarty->assign($outputData);
return $this->render(
'@ElcwebLegacy/Default/default.html.twig',
['output' => $smarty->fetch($template)]
);
}
public function bobAction()
{
$outputData = ['foo' => 'bar'];
return $this->renderSmarty('bob.tpl', $outputData);
}