Symfony Básico

Tópicos

  • Preparando infra para desenvolvimento
  • Model
  • Controller
  • View
  • Autenticação

Preparação da infra

Dica para começar:

mkdir vmsymfony
vagrant init bento/ubuntu-16.04

Adicionar IP para acesso externo:

config.vm.network :private_network, ip: "192.168.100.200"

Logar na nova VM:

vagrant up
vagrant ssh

Usar PHP em versões superiores >= 7.1.

Dica: usar o PPA do ondrej:

sudo add-apt-repository -y ppa:ondrej/php
sudo apt-get update

Instalação do PHP:

sudo apt-get -y install php7.2

Bibliotecas mínimas para o symfony:

sudo apt-get -y install php7.2-xml php7.2-intl php7.2-mbstring

Bibliotecas de conexão para bancos de dados:

sudo apt-get -y install php7.2-pgsql php7.2-mysql php7.2-sqlite3

Composer

Instalação do composer globalmente:

curl -s https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer

Instalação do git:

sudo apt-get install -y git

Configurações globais:

git config --global user.name "joazinho"
git config --global user.email "joaozinho@usp.br"

MariaDB

Dependências mínimas:

sudo apt-get -y install mariadb-server

Criando usuário e banco:

sudo mysql
CREATE DATABASE uspdev;
GRANT ALL PROVILEGES ON uspdev.* to uspdev@'localhost' identified by 'uspdev';
quit

PostgreSQL

Dependências mínimas:

sudo apt-get -y install postgresql

Criando usuário e banco:

sudo su posgtres
psql
CREATE USER uspdev WITH PASSWORD 'uspdev';
CREATE DATABASE uspdev OWNER uspdev;
\q
exit

Symfony

Projeto symfony e suas dependências:

composer create-project symfony/skeleton uspdev
cd usp
composer require doctrine maker annotations twig form validator
composer require encore asset webprofiler server web-server-bundle

Exemplo de entrada no .env para postgresql e mysql:

DATABASE_URL="pgsql://uspdev:uspdev@localhost:5432/uspdev?charset=utf8"
DATABASE_URL="mysql://uspdev:uspdev@localhost:3306/uspdev"
Documentação: https://symfony.com/doc/current/best_practices/creating-the-project.html
https://symfony.com/doc/current/setup.html
https://symfony.com/doc/master/configuration/external_parameters.html
http://symfony.com/doc/current/setup/built_in_web_server.html

Commit das mudanças:

git init
git add --all
git commit -m 'The best project in the world is alive'
git push origin master





Models

Criar seguintes issues no projeto:

  • Create Rede entity with fields: nome, iprede e cidr
  • Create get and set for all fields in Rede Entity
  • Create Equipamento entity with fields: patrimonio, macaddress, local, vencimento e ip. (vencimento type: datetimetz)
  • Create get and set for all fields in entity Equipamento

Checkout para branch master para issue1-RedeEntity

git checkout -b issue1-RedeEntity

Tabela Rede

php bin/console make:entity Rede

Aplicar schema no banco de dados:

php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate

devmaster:

  • Adicionar nome, iprede e cidr em Rede.php
  • Refatorar banco de dados
  • Commit e merge das mudanças


Documentação: https://symfony.com/doc/current/doctrine.html

Getters e Setters:

issue2: Exemplo de get e set pata o campo nome:

public function setNome($nome)
{
    $this->nome = $nome;
}

public function getNome()
{
    return $this->nome;
}

Tarefa:

  • Distribuição das issues 2,3 e 4 para os/as demais devs
  • Commits e push das resoluções das issues
  • Criação de pull/merge requests

Um equipamento pode pertencer a uma rede, certo? Então, na entidade Equipamento:

/**
 * @ORM\ManyToOne(targetEntity="Rede", inversedBy="equipamentos")
 * @ORM\JoinColumn(nullable=true)
 */
private $rede;

/* get e set */
public function setRede($rede)
{
    $this->rede = $rede;
}

public function getRede()
{
    return $this->rede;
}
Documentação: https://symfony.com/doc/current/doctrine/associations.html

Relacionamento inverso na entidade de Rede:

use Doctrine\Common\Collections\ArrayCollection;

public function __construct()
{
    $this->equipamentos = new ArrayCollection();
}

/**
 * @ORM\OneToMany(targetEntity="Equipamento",mappedBy="rede")
 */
private $equipamentos;


/* get e set */
public function setEquipamentos($equipamentos)
{
    $this->equipamentos = $equipamentos;
}

public function getEquipamentos()
{
    return $this->equipamentos;
}
Documentação: http://symfony.com/doc/current/form/form_collections.html

Controllers

Criar issues sobre controllers:

  • Create EquipamentoController
  • Create form EquipamentoType with fields: nome, iprede e cidr
  • Create newAction in EquipamentoController that receive request from EquipamentoType

Checkout para branch master para issues5-6-7-EquipamentoNewAction

git checkout -b issues5-6-7-EquipamentoNewAction

Criar controller para Equipamento:

php bin/console make:controller EquipamentoController

Subir um server local:

php -S 0.0.0.0:8888 -t public/
php bin/console server:run *:8888

Form para cadastro de equipamentos:

php bin/console make:form EquipamentoType

Adicionar os campos no form criado:

$builder->add('patrimonio')->add('macaddress')->add('local')
        ->add('vencimento');
Documentação: https://symfony.com/doc/current/controller.html
http://symfony.com/doc/current/forms.html

Rota para cadastrar novos equipamentos:

use App\Entity\Equipamento; 
use App\Form\EquipamentoType; 

Controler:

/**
 * @Route("/equipamento/new", name="equipamento_new")
 */
public function newAction()
{
    $equipamento = new Equipamento();
    $form = $this->createForm(EquipamentoType::class,$equipamento);
    return $this->render('equipamento/new.html.twig', array(
        'form' => $form->createView(),
    ));
}

Criar um arquivo de template
templates/equipamento/new.html.twig:

<html><body>
{{ form_start(form) }}
    {{ form_widget(form) }}
    <input type="submit" value="Cadastrar" />
{{ form_end(form) }}
</body></html>

Acessar rota no browser:

http://127.0.0.1:8000/equipamento/new

Sim, está horrível, mas aguardem!

Tratar a requisição no controller:

-> use Symfony\Component\HttpFoundation\Request;
-> public function newAction(Request $request)

Persistir campos no banco de dados:

$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
    $em = $this->getDoctrine()->getManager();
    $em->persist($equipamento);
    $em->flush();
    return $this->redirectToRoute('equipamento_new');
}

Persistiu?

php bin/console doctrine:query:sql "select * from equipamento;"

Finalizando issues:

  • devmaster: commit e merge da mudanças

Criação, distribuição e pull/merge requests das issues entre os/as devs:

  • Create RedeController
  • Create form RedeType with fields: nome, iprede e cidr
  • Create newAction in RedeController that receive request from RedeType form

Relacionamento entre equipamento e rede

  • Adicionar campo rede no form de equipamento
  • Boom? Método __toString na entity Rede:

    public funtion __toString() {

     return $this->nome;
    

    }

  • Cadastrar novo equipamento

  • Mágica: verificar no banco de dados!

nova issue: showAction e indexAction para EquipamentoController:

/**
 * @Route("/equipamento/{id}", name="equipamento_show")
 */
public function showAction(Equipamento $equipamento)
{
    return $this->render('equipamento/show.html.twig', array(
        'equipamento' => $equipamento,
    ));
}

Criar arquivo equipamento/show.html.twig:

<html><body> 
    {{ equipamento.macaddress}}
    {{ equipamento.vencimento|date('d/m/Y') }}
</body></html>

Criação de página que lista todos equipamentos:

/**
 * @Route("/equipamento", name="equipamento_index")
 */
public function indexAction()
{
    $repository = $this->getDoctrine()->getRepository(Equipamento::class);
    $equipamentos = $repository->findAll();
    return $this->render('equipamento/index.html.twig', array(
        'equipamentos' => $equipamentos,
    ));
}

Criar arquivo equipamento/index.html.twig:

<html><body><ul> 
{% for equipamento in equipamentos %} 
    <li> <a href="{{ path('equipamento_show', { 'id': equipamento.id }) }}"> {{equipamento.macaddress}}</a> </li>
{% endfor %}
</ul></body></html>

Filtrando equipamentos ativos via repository:

public function ativos()
{
    $now = new \DateTime();
    return $this->createQueryBuilder('e')
        ->where('e.vencimento >= :now')
        ->setParameter('now', $now)
        ->getQuery()
        ->getResult();
}

Corrigir controller

$equipamentos = $repository->ativos();

Ok, você quer usar SQL na raça...

public function ativosSql()
{
    $now_obj = new \DateTime();
    $now = $now_obj->format('Y-m-d H:i:s');
    $conn = $this->getEntityManager()->getConnection();
    $sql = 'SELECT * FROM equipamento e WHERE e.vencimento >= :now';
    $stmt = $conn->prepare($sql);
    $stmt->execute(['now' => $now]);
    return $stmt->fetchAll();
}

No Controller:

$equipamentos = $repository->ativosSql();

Criação, distribuição e pull/merge requests das issues entre os/as devs:

  • Create showAction and template for RedeController
  • Create indexAction and template for RedeController

Criação das seguintes issues para o/a devmaster:

  • Create editAction and template for EquipamentoController
  • Create deleteAction and template for EquipamentoController

Edição dos registros de equipamento:

/**
  * @Route("/equipamento/{id}/edit", name="equipamento_edit")
  */
public function editAction(Request $request, Equipamento $equipamento)
{
    $form = $this->createForm(EquipamentoType::class,$equipamento);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        $this->getDoctrine()->getManager()->flush();
        return $this->redirectToRoute('equipamento_show',['id' => $equipamento->getId()]);
    }

    return $this->render('equipamento/edit.html.twig', array(
        'equipamento' => $equipamento,
        'form' => $form->createView(),
    ));
}

Colocar link em equipamento/show.html.twig:

<a href="{{ path('equipamento_edit', { 'id': equipamento.id }) }}">Editar</a>

Para deletar, sério, nunca faça isso:

/**
 * @Route("/equipamento/{id}/delete", name="equipamento_delete")
 */
public function deleteAction(equipamento $equipamento) {
    $em = $this->getDoctrine()->getManager();
    $em->remove($equipamento);
    $em->flush();
    return $this->redirectToRoute('equipamento_index');
}

The Right Way: Criar um formulário para enviar requisição HTTP DELETE

use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
...
private function createDeleteForm(Equipamento $equipamento)
{
    $id = $equipamento->getId();
    return $this->createFormBuilder()
        ->setAction($this->generateUrl('equipamento_delete', ['id' => $id]))
        ->setMethod('DELETE')
        ->add('message',CheckboxType::class, [
             'label'    => "Quer mesmo deletar ?",
             'required' => true,])
        ->getForm();
}

Documentação: https://symfony.com/doc/current/form/without_class.html

Método HTTP DELETE

/**
 * @Route("/equipamento/{id}/delete", name="equipamento_delete",methods="DELETE")
 */
public function deleteAction(Request $request, Equipamento $equipamento)
{
    $form = $this->createDeleteForm($equipamento);
    $form->handleRequest($request);
    if ($form->isSubmitted() && $form->isValid()) {
        $em = $this->getDoctrine()->getManager();
        $em->remove($equipamento);
        $em->flush();
}
return $this->redirectToRoute('equipamento_index');

Tela de confirmação:

/**
 * @Route("/equipamento/{id}/delete/confirm", name="equipamento_delete_confirm")
 */
public function confirmDeleteAction(Equipamento $equipamento)
{
    $form = $this->createDeleteForm($equipamento);
    return $this->render('equipamento/delete.html.twig', array(
        'equipamento' => $equipamento,
        'form' => $form->createView(),
    ));
}

No template:

{{ form_start(form) }}
    <button type="submit">Deletar</button>
{{ form_end(form) }}

Criação, distribuição e pull/merge requests das issues entre os/as devs:

  • Create editAction and template fir RedeController
  • Create deleteAction and template fir RedeController

Dica para deixar equipamentos órfãos:

$equipamentos = $rede->getEquipamentos();
foreach($equipamentos as $equipamento){
    $equipamento->setRede(null);
}

Modelo para dhcp.conf para múltiplas networks:

ddns-update-style none;
default-lease-time 86400;
max-lease-time 86400;
authoritative;
option domain-name-servers 143.107.253.3,143.107.253.5;

shared-network "default" {

    subnet 10.0.184.0 netmask 255.255.255.0 {
        range 10.0.184.2 10.0.184.254;
        option routers 10.0.184.1;          
        option broadcast-address 10.0.184.255;
        deny unknown-clients;
        host cliente1 {
            hardware ethernet 00:1C:C0:99:0A:04;
            fixed-address 10.0.184.51;
        }
    }

   subnet 10.0.188.0 netmask 255.255.255.0 {
        range 10.0.188.2 10.0.188.254;
        option routers 10.0.188.1;
        option broadcast-address 10.0.188.255;
        deny unknown-clients;
        host cliente2 {
            hardware ethernet 00:1C:C0:98:FB:3C;
            fixed-address 10.0.188.69;
        }
    }
}

Controller que monta o dhcp.conf:

/**
 * @Route("/dhcpconf", name="dhcpconf")
 */
public function dhcpconf()
{
    $response = new Response('teste');
    $response->headers-> set('Content-Type', 'text/plain'); 
    return $response;
}

Em src/Service/Dhcpconf.php

namespace App\Service;
class Dhcpconf
{
    public function build()
    {
        return "ok, I amworking";
    }
}

No Controller:

use App\Service\Dhcpconf;
$dhcpconf = new Dhcpconf();
$dhcpconf->build();

Necessidades: A partir do ip da rede e cidr:

  • Encontrar primeiro ip (gateway) e broadcast
  • Converter cidr para máscara
  • Encontrar range

Instalando e usando biblioteca externa:

#https://github.com/S1lentium/IPTools
sudo apt-get install php7.2-bcmath
composer require s1lentium/iptools

Classes disponíveis nessa lib:

use IPTools\IP;
use IPTools\Network;

Array com IPs do range:

$network = Network::parse('192.168.1.0/24');
$ips = [];
foreach($network as $ip) {
    array_push($ips,(string)$ip);
}
// todo: gateway,broadcast,range_begin,range_end

Issues:

  • Allocate available IP to Equipamento in newAction/editAction
  • Generate dhcp.conf

Symfony Básico - Parte 1 - hasMany relationship e psych

Tenha instalado:

php7.1+, composer, mysql e git*

composer create-project symfony/skeleton symfony_basico

symfony require maker

symfony require doctrine

composer require theofidry/psysh-bundle

create database symfony; grant all privileges on symfony.* to symfony@localhost identified by 'symfony'; inserir informações no .env

php bin/console make:entity Post php bin/console doctrine:migrations:diff php bin/console doctrine:migrations:migrate show tables

php bin/console make:entity Comment php bin/console doctrine:migrations:diff php bin/console doctrine:migrations:migrate

Adicionar os campos title e content na entity Post (as public) Adicionar os campos title, content, author_email e post_id (unsigned e referenciando um post) na entity Comment (as public) @ORM\ManyToOne(targetEntity="Rede", inversedBy="equipamentos")

Frizar que no relacionamento do symfony, a configuração vale tanto para o modelo quando para o db

use Doctrine\Common\Collections\ArrayCollection; public function __construct() { $this->comments = new ArrayCollection(); } /**

 * @ORM\OneToMany(targetEntity="Equipamento",mappedBy="rede")
 */
private $equipamentos;

Array collection em POst apontando para Comments

php bin/console doctrine:migrations:diff php bin/console doctrine:migrations:migrate show tables desc Comment; desc Post;

php bin/console psysh

$post = new App\Entity\Post $post->title = "primeiro post" $post->content = "texto maravilhoso sobre um super post."

ls $em = $container->get('doctrine')->getManager() $em->persist($post) $em->flush()

$p = $em->getRepository('App\Entity\Post') $posts = $p->findAll()

listar todos títulos

foreach ($posts as $post) echo $post->title

$comment = new App\Entity\Comment $comment->author_email = 'fulano@eu.com' $comment->title = 'meu comentário' $comment->content = 'Gostei muito do seu post' $comment->post_id = $posts['id'==1] $em->persist($comment) em->flush()

select * from comment;

$comment2 = new App\Entity\Comment $comment2->author_email = 'ciclano@eu.com' $comment2->title = 'meu comentário 2' $comment2->content = 'Eu não gostei' $comment2->post = $posts['id'==1] $em->persist($comment2) $em->flush()

select * from comment;

$c = $em->getRepository('App\Entity\Comment') $comments = $c->findAll()

select * from Comment;

$p->find(1)->comments->toArray()

$c->find(2) $c->find(2)->post $c->find(2)->post->title

[extra] $em->createQuery('SELECT A.title FROM App:Post A WHERE A.id=2')->getResult()

$qb = $em->createQueryBuilder() $qb->select('u')->from('App\Entity\Post', 'u')->where('u.id = 2')->getQuery()->getResult()

Criar um método em src/Repository/CommentRepository.php e testar no console

####################################################3

Laravel Básico - Parte 2 (hasManyThrough)

Criar o model Authors

  • php artisan make:model Author -m
  • Colocar as colunas na tabela authors em database/migrations/create_authors_table.php
      public function up()
      {
          Schema::create('authors', function (Blueprint $table) {
              $table->increments('id');
              $table->string('name');
              $table->string('email');
              $table->string('bio')->nullable();
              $table->timestamps();
          });
      }
    

Adicionar o ID do autor ao Post

  • php artisan make:migration add_author_id_to_posts --table=posts
  • Colocar a coluna de chave estrangeira em database/migrations/add_author_id_to_posts.php

      public function up()
      {
              Schema::table('posts', function (Blueprint $table) {
                  $table->integer('author_id')->unsigned();
    
                  $table->foreign('author_id')->references('id')->on('authors');
              });
          }
    
          /**
           * Reverse the migrations.
           *
           * @return void
           */
          public function down()
          {
              Schema::table('posts', function (Blueprint $table) {
                  $table->dropColumn('author_id');
              });
          }
      }
    

Rodar as migrations

  • Antes de rodar as migrations, temos que dar refresh no banco de dados, pois podem existir dados que impediriam a adição de novas chaves estrangeiras (PDOException::("SQLSTATE[23000]):
    • php artisan migrate:refresh
    • php artisan migrate

Adicionar os relacionamentos

  • app/Post.php

      <?php
    
      namespace App;
    
      use Illuminate\Database\Eloquent\Model;
    
      class Post extends Model
      {
          public function comments()
          {
              return $this->hasMany('App\Comment');
          }
    
          public function author()
          {
              return $this->belongsTo('App\Author');
          }
      }
    
    • app/Author.php

        <?php
      
        namespace App;
      
        use Illuminate\Database\Eloquent\Model;
      
        class Author extends Model
        {
            public function posts()
            {
                return $this->hasMany('App\Post');
            }
            public function comments()
            {
                return $this->hasManyThrough('App\Comment', 'App\Post');
            }
        }
      

Sugestões para testar o banco de dados no Tinker

- php artisan tinker
- $author = new Author
- $author->name = 'Leandro Ramos'
- $author->email = 'leandroramos@usp.br'
- $author->save()
- $post = new Post
- $post->title = 'Um Grande Post!'
- $post->content = 'Um excelente texto explicando sobre uma grande novidade'
- $post->author_id = 1
- $post = new Post
- $post->title = 'Segundo Grande Post!'
- $post->content = 'Segundo texto espetacular sobre um grande post.'
- $post->author_id = 1
- $post = new Post
- $post->title = 'Um Grande Post!'
- $post->content = 'Um excelente texto explicando sobre uma grande novidade'
- $post->author_id = 1 
- $post->save()
- $post = new Post
- $post->title = 'Segundo Grande Post!'
- $post->content = 'Segundo texto espetacular sobre um grande post.'
- $post->author_id = 1 
- $post->save()
- $comment = new Comment
- $comment->author_email = 'comentador@site.com'
- $comment->content = 'Mas que post sensacional!'
- $comment->post_id = 1
- $comment->save()
- $comment = new Comment
- $comment->author_email = 'oriboncina@site.com'
- $comment->content = 'Sei não... estás enganado!!'
- $comment->post_id = 1
- $comment->save()
- $comment = new Comment
- $comment->author_email = 'eu@site.com'
- $comment->content = 'Estão faltando alguns links no post.'
- $comment->post_id = 1
- $comment->save()
- $comment = new Comment
- $comment->author_email = 'eu@site.com'
- $comment->content = 'Obrigado, ajudou muito.'
- $comment->post_id = 2
- $comment->save()
- 
- Mais testes:
    - $author = Author::first()
    - $author->posts
    - $author->comments
    - $author->comments->where('post_id', '2')
    - $comment = Comment::first()
    - $comment->post
    - $comment->post->author
    - $comment->post->author->name

Links de referência

Parte 3 - Factories

Criar as factories

- php artisan make:factory AuthorFactory --model="App\\Author"
- php artisan make:factory PostFactory --model="App\\Post"
- php artisan make:factory CommentFactory --model="App\\Comment"

Código das factories

  • AuthorFactory

      <?php
    
      use Faker\Generator as Faker;
    
      $factory->define(App\Author::class, function (Faker $faker) {
          return [
              'name'  => $faker->name,
              'email' => $faker->unique()->safeEmail,
              'bio'   => $faker->paragraph(1),
          ];
      });
    
  • PostFactory

      <?php
    
      use App\Author;
      use Faker\Generator as Faker;
    
      $factory->define(App\Post::class, function (Faker $faker) {
          return [
              'title'     => $faker->sentence(4),
              'content'   => $faker->paragraph(4),
    
              'author_id' => function () {
                  return Author::orderByRaw("RAND()")
                      ->take(1)
                      ->first()
                      ->id;
              }
          ];
      });
    
  • CommentFactory

      <?php
    
      use App\Post;
      use Faker\Generator as Faker;
    
      $factory->define(App\Comment::class, function (Faker $faker) {
          return [
              'author_email'  => $faker->unique()->safeEmail,
              'content'       => $faker->paragraph(2),
    
              'post_id' => function () {
                  return Post::orderByRaw("RAND()")
                      ->take(1)
                      ->first()
                      ->id;
              }
          ];
      });
    

Código do DatabaseSeeder

  • database/seeds/DatabaseSeeder.php

      <?php
    
      use Illuminate\Database\Seeder;
    
      class DatabaseSeeder extends Seeder
      {
          /**
           * Run the database seeds.
           *
           * @return void
           */
          public function run()
          {
              // $this->call(UsersTableSeeder::class);
              echo "Criando 10 autores...\n";
              factory(App\Author::class, 10)->create();
    
              echo "Criando 36 posts relacionados a autores aleatórios...\n";
              factory(App\Post::class, 36)->create();
    
              echo "Criando 67 comentários relacionados a posts aleatórios...\n";
              factory(App\Comment::class, 67)->create();
          }
      }
    

Links de referência