このエントリーをはてなブックマークに追加

こんにちは!Symfonyアドベントカレンダー 18日目です :christmas_tree::crescent_moon:

今回は、Symfony4を使って、簡単なフォームを作成してみたいと思います!

こちらの過去記事が参考になりました! :point_right:Symfony4をインストールして”Hello World”を表示させるまでの手順

動作環境

  • Symfony4.4.0

Symfony Standard Edition → Symfony Flex

Symfony4では、Symfony Standard Editionはサポート対象外となり、Symfony Flexを採用しています。

  • composer.json
    "require": {
        "symfony/flex": "^1.3.1",
    },
    "conflict": {
        "symfony/symfony": "*"
    },

手順

プロジェクトの作成

$ composer create-project symfony/website-skeleton hello-symfony4

Installing symfony/website-skeleton (v4.4.99)
  - Installing symfony/website-skeleton (v4.4.99): Downloading (100%)
Created project in hello-symfony4
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Installing symfony/flex (v1.4.8): Loading from cache
Symfony operations: 1 recipe (92084fadae9fb1e6399edca5469bc31d)
  - Configuring symfony/flex (>=1.0): From github.com/symfony/recipes:master
Loading composer repositories with package information
Updating dependencies (including require-dev)
Restricting packages listed in "symfony/symfony" to "4.4.*"
...

symfony/flexをインストールしてますねえ)

env(DB)設定

ローカル用のenvファイルを作ります。

$ cp .env .env.local

ローカルDBの接続情報に書き換えます

$ vi .env.local

きちんと接続されているか確認します

$ bin/console doctrine:migrations:status

 == Configuration

    >> Name:                                               Application Migrations
    >> Database Driver:                                    pdo_mysql
    >> Database Host:                                      127.0.0.1
    >> Database Name:                                      hello_symfony4 //指定したDB名がはいります
...

serverをいれる

ローカルサーバを立てるコマンドが欲しいので導入します。

$ composer req server

Symfony Flexのおかげで、serverというエイリアスでインストールが可能となっています。

これで無事、ローカルでwebサイトの表示確認ができるようになりました。

$ bin/console server:start


 [OK] Server listening on http://127.0.0.1:8000


コマンドが不要な場合は、$ php -S 127.0.0.1:8000 -t public/ でも確認ができます。

Symfony4.4ではWelcomeページがガラッと変わりカッコよくなりましたね!!!

いろんなカラーバージョンがあるようなので、何回かリロードしてみてください :ok_woman:

127 0 0 1_8002_

Symfony Profilerがダークモード対応してますねえ!目に優しくていいです :ok_hand:

127 0 0 1_8002__profiler_c20a81_panel=exception

コントローラを作る

$ bin/console make:controller FormController

 created: src/Controller/FormController.php
 created: templates/form/index.html.twig

           
  Success! 
           

 Next: Open your new controller class and add some pages!

これだけで、こんな画面ができます。

127 0 0 1_8002_form

フォームを作る

  • src/Controller/CommentController.php
<?php


namespace App\Controller;

use ...

class CommentController extends AbstractController
{
    /**
     * @Route("/comment", name="comment")
     * @param Request $request
     * @return Response
     */
    public function index(Request $request): Response
    {
        $message = '↑ input, and submit';
        $form = $this->createFormBuilder()
            ->add('name')
            ->add('comment')
            ->add('submit', SubmitType::class)
            ->getForm();

        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $data = $form->getData();
            $message = "Thanks, {$data['name']}";
        }

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

  • templates/comment/index.html.twig
{% extends 'base.html.twig' %}

{% block title %}Hello Symfony4!{% endblock %}

{% block body %}
    <style>
        .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
        .example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
    </style>

    <div class="example-wrapper">
        <h1>Hello, Symfony Advent Calendar 18th!!</h1>

        {{ form(form) }}
        {{ message }}
    </div>
{% endblock %}

ついに、簡単なフォームが完成しました :tada:

スクリーンショット 2019-12-17 23 46 58

最後に

フォームに関してSymfony4.0よりif($form->isValid())のみだとCannot check if an unsubmitted form is valid. Call Form::isSubmitted() before Form::isValid(). と言われちゃいます。

また、Symfony4.4からWeek Form Typeというものが追加されたみたいです。次回はこのことについて書きたいと思います!!!

個人的には、Welcomeページが4.3→4.4で大きくリニューアルされていてびっくりしました!

ダークモードもあったりで、ますます開発しやすくなって嬉しいです。ありがとうございます :bow:


このエントリーをはてなブックマークに追加

こんにちは!下田です。

ところでPHPerの皆様!PsySH使ってますでしょうか!

手元でちょっとしたスクリプトを書いて動かしたり、Nagoya.phpで出題される「どう書く」問題のお供に使ったり、何かと便利なやつです。

詳しい説明は公式ページに譲るとして、今日はPsySHをSymfonyで使う際のちょっとしたtipsを共有します。

インストール

Symfony2系の頃からあるPsyshBundleなんてのもありますが、僕はいつも素のままでcomposer requireしています。

$ composer require --dev psy/psysh

新規プロジェクトをgit cloneした直後など、思考停止で即インストールしてます。

ブレークポイントとして使う例

ローカル開発で特に捗るのですが、処理の途中でeval(\Psy\sh());と書くと、そこで処理が停止してコンソールが開きます。

例えばこんなスクリプトを書いて(中身は適当)

<?php

require __DIR__.'/vendor/autoload.php';

$zipped = array_map(null, [1,2,3], ['a','b','c'], ['foo','bar','baz']);

eval(\Psy\sh());

実行してみましょう。

% php test.php

Psy Shell v0.9.12 (PHP 7.3.12 — cli) by Justin Hileman

From test.php:7:

    5| $zipped = array_map(null, [1,2,3], ['a','b','c'], ['foo','bar','baz']);
    6|
  > 7| eval(\Psy\sh());
    8|
>>> 

ここで入力待ち状態になります。 $zippedの中身を見てみましょう。

>>> $zipped
=> [
     [
       1,
       "a",
       "foo",
     ],
     [
       2,
       "b",
       "bar",
     ],
     [
       3,
       "c",
       "baz",
     ],
   ]
>>> 

わーい!完璧ですね。

Symfonyで使ってみる

こんな調子で、ローカルでのSymfony開発にも使ってみましょう。

試しにRequestオブジェクトの中身を見てみたいと思います。

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class FooController extends AbstractController
{
    public function fooAction(Request $request): Response
    {
        eval(\Psy\sh());
        // ...
    }
}

おもむろにサーバを起動し、

% symfony server:start

ブラウザでfooActionにアクセスした後、コンソールに戻って見てみます。

|WARN | PHP    child 9777 said into stdout: "Psy Shell v0.9.12 (PHP 7.3.12 — fpm-fcgi) by Justin Hileman"
|WARN | PHP    child 9777 said into stdout: ""
|WARN | PHP    child 9777 said into stdout: "From /your/project/src/Controller/FooController.php:18:"
|WARN | PHP    child 9777 said into stdout: ""
|WARN | PHP    child 9777 said into stdout: "    16|     public function fooAction(Request $request): Response"
|WARN | PHP    child 9777 said into stdout: "    17|     {"
|WARN | PHP    child 9777 said into stdout: "  > 18|         eval(\Psy\sh());"
|WARN | PHP    child 9777 said into stdout: ""
|WARN | PHP    child 9777 said into stdout: "Exit:  Ctrl+D"

おや。入力待ちにならずにスルーされてしまいました。

どうやらsymfonyコマンドでサーバを起動すると、プロセスをフォークした先の子プロセス側でpsyshが発動してしまうらしく、うまく止まってくれません。

そこでちょっと工夫

実は、symfonyコマンドを使わずとも、PHPのビルトインサーバを直接使ってsymfonyを起動することができます。

バージョン4系なら以下です。

% php -S 127.0.0.1:8000 public/index.php
Psy Shell v0.9.12 (PHP 7.3.12 — cli-server) by Justin Hileman

From /your/project/src/Controller/FooController.php:18


    16|     public function fooAction(Request $request): Response
    17|     {
  > 18|         eval(\Psy\sh());
    19|
>>>

やった!捗りますね。

SymfonyはProfilerが強力なので、もともとデバッグしやすい類のFWだと思いますが、

こういったツールを使うことで処理の流れやコードリーディングなどの際にも役に立つかもしれませんね。


このエントリーをはてなブックマークに追加

こんにちは!下田です。

Symfony Advent Calendar 11日目の記事です。若干の時空間異常が起きているようですが気にしないでおきます。

さて今回はみんな大好き(?)Inheritance Mappingの話です。

2015年の記事ではありますが、弊社ブログでも過去に紹介しております。

リレーションと組み合わせて使うとすごい

今日紹介したかったのはこちらです。「リレーションと組み合わせるとすごい」という話です。

以下、簡単な例で説明します。

前提

Department(部署)クラスとEmployee(社員)クラスがあり、部署クラスに複数の社員クラスがOneToManyで紐付いている状況を考えます。

Departmentクラス

<?php

namespace App\Entity;

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

/**
 * 部署クラス
 * @ORM\Entity
 */
class Department
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @var Collection<Employee>
     * @ORM\OneToMany(targetEntity="Employee",mappedBy="department", fetch="EAGER")
     */
    private $employees;

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

    /**
     * @return Collection
     */
    public function getEmployees(): Collection
    {
        return $this->employees;
    }
}

Employeeクラス

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * 社員クラス
 * @ORM\Entity
 */
abstract class Employee
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity="Department", inversedBy="employees")
     * @ORM\JoinColumn(name="department_id", referencedColumnName="id")
     */
    private $department;
}

但し、社員クラスはRegularEmployee(正社員),ContractEmployee(契約社員)の2種類があり、Inheritance Mappingで表現されているものとします。

Employeeクラスにmapping設定を書いて、

/**
 * @ORM\Entity
+ * @ORM\InheritanceType("SINGLE_TABLE")
+ * @ORM\DiscriminatorColumn(name="type", type="string")
+ * @ORM\DiscriminatorMap({"regular" = "RegularEmployee", "contract" = "ContractEmployee"})
 */
abstract class Employee
{
}

RegularEmployee,ContractEmployeeクラスを定義します。

<?php


namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * 正社員
 * @ORM\Entity
 */
class RegularEmployee extends Employee
{
}
<?php


namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * 契約社員
 * @ORM\Entity
 */
class ContractEmployee extends Employee
{
}

図にするとこんな関係ですね。

class


さてこのとき、部署クラスの getEmployee() で取得できるのは

  • (正社員,契約社員の集合である)社員クラスのCollection

ですが、

  • 正社員のみのCollection
  • 契約社員のみのCollection

をそれぞれ取得したい場合も出てくると思います。さて、どう実装すればよいでしょうか?


結論から述べると、普通にMappingアノテーションのみで定義できちゃうんです。 やってみます。

<?php

namespace App\Entity;

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

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

    /**
     * 社員の集合
     * @var Collection<Employee>
     * @ORM\OneToMany(targetEntity="Employee",mappedBy="department", fetch="EAGER")
     */
    private $employees;

    /**
     * 正社員のみの集合
     * @var Collection<RegularEmployee>
     * @ORM\OneToMany(targetEntity="RegularEmployee",mappedBy="department", fetch="EAGER")
     */
    private $regularEmployees;

    /**
     * 契約社員のみの集合
     * @var Collection<ContractEmployee>
     * @ORM\OneToMany(targetEntity="ContractEmployee",mappedBy="department", fetch="EAGER")
     */
    private $contractEmployees;

    public function __construct()
    {
        $this->employees = new ArrayCollection();
        $this->regularEmployees = new ArrayCollection();
        $this->contractEmployees = new ArrayCollection();
    }

    /**
     * @return Collection
     */
    public function getEmployees(): Collection
    {
        return $this->employees;
    }

    /**
     * @return Collection
     */
    public function getRegularEmployees(): Collection
    {
        return $this->regularEmployees;
    }

    /**
     * @return Collection
     */
    public function getContractEmployees(): Collection
    {
        return $this->contractEmployees;
    }
}

こんな感じで、Employee/RegularEmployee/ContractEmployeeをそれぞれプロパティとして定義しておくと、きちんと所望のクラス別に分けて取得することができます。

正/契約社員クラスに対しての OneToManyを定義したので、正/契約社員クラス側にも ManyToOneを書いておきましょう。

class RegularEmployee extends Employee
{
    /**
     * 所属部署
     * @ORM\ManyToOne(targetEntity="Department", inversedBy="regularEmployees")
     */
    private $department;
}
class RegularEmployee extends Employee
{
    /**
     * 所属部署     
     * @ORM\ManyToOne(targetEntity="Department", inversedBy="regularEmployees")
     */
    private $department;
}

動作確認

実際に取得できるか確かめてみます。一旦Repositoryを作っておいて、

<?php

namespace App\Repository;

use App\Entity\Department;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Common\Persistence\ManagerRegistry;

class DepartmentRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Department::class);
    }
}
<?php

namespace App\Repository;

use App\Entity\Employee;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Common\Persistence\ManagerRegistry;

class EmployeeRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Employee::class);
    }
}

DBを更新し、実際にデータを入れてみます。

INSERT INTO `department` (`id`)
VALUES (1);

INSERT INTO `employee` (`id`, `type`, `department_id`)
VALUES
	(1, 'regular', 1),
	(2, 'regular', 1),
	(3, 'regular', 1),
	(4, 'contract', 1),
	(5, 'contract', 1),
	(6, 'contract', 1);

departmentテーブル

id
1

Department(部署)はid=1の一つだけです。

employeeテーブル

id department_id type
1 1 regular
2 1 regular
3 1 regular
4 1 contract
5 1 contract
6 1 contract

正社員,契約社員がそれぞれ3名ずつ、合計6名の社員がDepartment(部署) id=1にぶら下がっている形です。

これでデータの準備は完了です。

さて、確認方法は何でもよいのですが、DepartmentControllerをシュッと作って、dumpして見てみましょう。

<?php

namespace App\Controller;

use App\Entity\Employee;
use App\Entity\RegularEmployee;
use App\Entity\ContractEmployee;
use App\Entity\Department;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class DepartmentController extends AbstractController
{
    /**
     * @Route("/department", name="department")
     */
    public function index()
    {
        /** @var Department $department */
        $department = $this->get('doctrine')->getRepository(Department::class)->find(1);

        dump($department->getEmployees());
        dump($department->getRegularEmployees());
        dump($department->getContractEmployees());

        // ...
    }
}

https://127.0.0.1:8000/department にアクセスしてみます。

おお!

  • (正社員,契約社員の集合である)社員クラスのCollection
  • 正社員のみのCollection
  • 契約社員のみのCollection

をそれぞれ取得できているようですね。Doctrineは賢い!

Repository使ってもすごい

ついでに、部署からのリレーションではなくRepositoryから取得する場合を考えます。先程作ったコレですね。

<?php

namespace App\Repository;

use App\Entity\Employee;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Common\Persistence\ManagerRegistry;

class EmployeeRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Employee::class);
    }
}

さて、正/契約社員をどう出し分けたら良いでしょうか?

…typeで判別する?

…RegularEmployeeRepository,ContractEmployeeRepositoryをそれぞれ作る必要がある?


正解はこれです。

$employeeRepo = $this->get('doctrine')->getRepository(Employee::class);
$regularEmployeeRepo = $this->get('doctrine')->getRepository(RegularEmployee::class);
$contractEmployeeRepo = $this->get('doctrine')->getRepository(ContractEmployee::class);

EmployeeRepositoryさえあれば、getRepository()する時に具象クラス名を指定すれば、各々の具象クラスに対してのRepositoryとして働いてくれます。

やってみます。さっきのDepartmentControllerを書き換えて、

<?php

namespace App\Controller;
class DepartmentController extends AbstractController
{
    /**
     * @Route("/department", name="department")
     */
    public function index()
    {
        $employee = $this->get('doctrine')->getRepository(Employee::class)->findAll();
        $regularEmployee = $this->get('doctrine')->getRepository(RegularEmployee::class)->findAll();
        $contractEmployee = $this->get('doctrine')->getRepository(ContractEmployee::class)->findAll();
        
        dump($employee);
        dump($regularEmployee);
        dump($contractEmployee);

        // ...
    }
}

この状態で https://127.0.0.1:8000/department を見てみましょう。

 2019-12-18 9.52.51.png (143.0 kB)

きちんと区別して取得できていますね。

(でもこれ、Controllerだからできる事であって、RepositoryをDIする利用法と相性悪いような気もしますが…)

おわりに

今回はInheritance Mappingについての話でした。

ついついテーブル設計側(DB側)に着目してしまいがちですが、利用するアプリケーション側の挙動もきちんと考えられていて、Doctrineはすごいなぁと。

みんな大好き(?)と冒頭では疑問形で書きましたが、この記事で少しDoctrineのことを好きになってもらえれば幸いです。

今回の例をgithubに上げておいたので、ご興味ある方はぜひ触ってみて下さい!