[go: up one dir, main page]
More Web Proxy on the site http://driver.im/ PHP Mentors (Posts tagged symfony)
1.5M ratings
277k ratings

See, that’s what the app is perfect for.

Sounds perfect Wahhhh, I don’t wanna

Practical Symfony #24: ダイナミックなコンフィギュレーショングラマー

Symfonyフレームワークの動作を設定するコンフィギュレーションとその背後の仕組みは、Symfonyのアーキテクチャを支える強力な屋台骨となっています。この仕組みの応用例の1つとして、コンフィギュレーションエントリをダイナミックに定義する仕掛けを見てみます。

通常のコンフィギュレーション

通常の静的なコンフィギュレーションは、バンドル内のDependencyInjectionディレクトリ以下にConfigurationクラスを用意し、そこで定義されたツリー構造に従って読み込まれます。コンフィギュレーションクラスの例は設定の仕様とは等を参照してください。この場合、あらかじめ固定のコンフィギュレーショングラマーがあり、それにもとづいてコンフィギュレーションファイルに設定を記述し、そのファイルの設定を読み込んでサービスコンテナが動作します。

configuration1

アプリケーション開発の多くの場面ではこのような静的な定義で十分ですが、コンフィギュレーションで記述したい内容が、必ずしも1つのバンドルの内容だけで決まるとは限りません。他のバンドルと疎結合な状態を保ちつつ、協調して機能するようなコンフィギュレーションはどうやって定義するのでしょうか。

Securityバンドルのエンティティプロバイダの例

他のバンドルと協調動作するコンフィギュレーションの例として、Securityバンドルのエンティティプロバイダの例を紹介します。Symfonyで認証ユーザーのプロバイダーとしてDoctrineのエンティティクラスを使う場合、次のようにsecurity.ymlのprovidersに定義します。

    providers: 
        my_entity_provider:
            entity:
                class:              SecurityBundle:User
                property:           username
                manager_name:       ~

この定義でentityというのがプロバイダの種類を表しており、他にはmemoryがあります。entityの場合はSecurityバンドルの機能とDoctrineの機能が連携して動作しますが、そういったDoctrineを前提とした機能をSecurityバンドルが抱え込んでいるのでしょうか? ちなみに、EntityUserProviderクラスはDoctrineBridgeという連携パッケージにあります。

このダイナミックな定義の流れは、次の図のようになっています。

configuration2

Securityバンドルにおけるエンティティプロバイダのグラマー定義は、Securityエクステンションが保持しているプロバイダーファクトリーによって決まるようになっています。Securityバンドルには、このファクトリーのためのインターフェイスが定義されています。

SecurityBundle / DependencyInjection / Security / UserProvider / UserProviderFactoryInterface.php

interface UserProviderFactoryInterface
{
    public function create(ContainerBuilder $container, $id, $config);

    public function getKey();

    public function addConfiguration(NodeDefinition $builder);
}

エクステンションは、コンフィギュレーションの仕組みが動作する前に読み込まれています。Doctrineバンドルのbuild()メソッドで、Securityエクステンションがロードされている場合は、Securityエクステンションへエンティティプロバイダーファクトリーを登録しています。

DoctrineBundle / DoctrineBundle.php

    public function build(ContainerBuilder $container)
    {
        parent::build($container);

        $container->addCompilerPass(new RegisterEventListenersAndSubscribersPass('doctrine.connections', 'doctrine.dbal.%s_connection.event_manager', 'doctrine'), PassConfig::TYPE_BEFORE_OPTIMIZATION);

        if ($container->hasExtension('security')) {
            $container->getExtension('security')->addUserProviderFactory(new EntityFactory('entity', 'doctrine.orm.security.user.provider'));
        }
        $container->addCompilerPass(new DoctrineValidationPass('orm'));
    }

プロバイダーファクトリーとして登録されるEntityFactoryは、コンフィギュレーションノードを追加するメソッド(addConfiguration())を持っています。

Bridge / Doctrine / DependencyInjection / Security / UserProvider / EntityFactory.php

class EntityFactory implements UserProviderFactoryInterface
{
    ...
    public function addConfiguration(NodeDefinition $node)
    {
        $node
            ->children()
                ->scalarNode('class')->isRequired()->cannotBeEmpty()->end()
                ->scalarNode('property')->defaultNull()->end()
                ->scalarNode('manager_name')->defaultNull()->end()
            ->end()
        ;
    }
}

Securityバンドルのグラマー準備段階で、登録されたプロバイダーファクトリーのノード追加メソッドを呼び出しています($factory->addConfiguration())。

SecurityBundle / DependencyInjection / MainConfiguration.php

        foreach ($this->userProviderFactories as $factory) {
            $name = str_replace('-', '_', $factory->getKey());
            $factoryNode = $providerNodeBuilder->children()->arrayNode($name)->canBeUnset();

            $factory->addConfiguration($factoryNode);
        }

これにより、security.ymlを読み込む段階では、グラマー定義としてentityという項目がすでに有効になっている状態になります。コンフィギュレーションファイルが読み込まれ、エンティティプロバイダーが有効になります($factory->create())。

SecurityBundle / DependencyInjection / SecurityExtension.php

    private function createUserDaoProvider($name, $provider, ContainerBuilder $container, $master = true)
    {
        $name = $this->getUserProviderId(strtolower($name));

        foreach ($this->userProviderFactories as $factory) {
            $key = str_replace('-', '_', $factory->getKey());

            if (!empty($provider[$key])) {
                $factory->create($container, $name, $provider[$key]);

                return $name;
            }
        }

(しかし、ここはサービスコンテナのコンパイル段階であって、有効になるといってもエンティティプロバイダーのサービス定義が追加されるのみです。アプリケーションのランタイムまで、実際のオブジェクトはインスタンス化されません)

Bridge / Doctrine / DependencyInjection / Security / UserProvider / EntityFactory.php

class EntityFactory implements UserProviderFactoryInterface
{
    public function create(ContainerBuilder $container, $id, $config)
    {
        $container
            ->setDefinition($id, new DefinitionDecorator($this->providerId))
            ->addArgument($config['class'])
            ->addArgument($config['property'])
            ->addArgument($config['manager_name'])
        ;
    }

まとめ

サービスコンテナと、コンフィギュレーションとエクステンションの動きが見えてくると、Symfonyがどのように作られ動いているのか、把握しやすくなります。Securityバンドルはこの仕組みを最も高度に利用しているバンドルの1つです。ソースを読んでみるとさまざまな発見があります。

参考

practical.symfony configuration symfony dsl

設定の仕様とは

設定の仕様をどう表現するのか、という趣旨の記事がありました。



設定はDSLである

このブログの過去記事(Pieceの中のSymfony #4: Configコンポーネント)やWEB+DB PRESS vol.75でも書いていますが、設定とはDSLです。ですから、設定の仕様というのはDSLの言語仕様として表現するのが適切です。DSLという抽象によって、内部でどのように問題が解決されるのかということと、設定の記述とが切り離されます。


PHPでの実装例(Symfony/Config利用)

以降はPHPでの例になりますが、過去記事と同じくSymfony/Configコンポーネントを利用して、同じ設定ファイルを読み込むDSLを実装してみましょう(ソース)。

src/BlogConfiguration.php クラスに、コンフィギュレーションの構文(ツリー)を定義します。

<?php
namespace Example\Config;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Respect\Validation\Validator as v;

class BlogConfiguration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('blog');

        $rootNode
            ->children()
                ->arrayNode('user_blogs')
                    ->isRequired()
                    ->requiresAtLeastOneElement()
                    ->prototype('array')
                    ->children()
                        ->scalarNode('blog_url')
                            ->info('設定したいブログのURL')
                            ->isRequired()
                            ->validate()
                                ->ifTrue(function ($value) {
                                    return !v::call(
                                        'parse_url',
                                        v::arr()->key('scheme', v::startsWith('http'))
                                            ->key('host',   v::domain())
                                    )->validate($value);
                                })
                                ->thenInvalid('ブログURLが無効: %s')
                            ->end()
                        ->end()
                        ->enumNode('permission')
                            ->info('publicなら公開、privateなら非公開')
                            ->values(['public', 'private'])
                            ->isRequired()
                        ->end()
                        ->arrayNode('can_be_edited_by')
                            ->info('編集権限を持つユーザ')
                            ->isRequired()
                            ->requiresAtLeastOneElement()
                            ->prototype('scalar')
                        ->end()
                    ->end()
                ->end()
            ->end();

        return $treeBuilder;
    }
}

ターゲットがJSONファイルなので、JSONファイル用のローダークラスを用意します(src/JsonFileLoader.php)。

元記事の例では配列がむき出しで定義されていましたが、私の例ではblog.user_blogsというルートノードを想定しています。ローダーでこの差を吸収しています。

<?php
namespace Example\Config;

use Symfony\Component\Config\Definition\Processor;
use Symfony\Component\Config\Loader\FileLoader;

class JsonFileLoader extends FileLoader
{
    public function load($resource, $type = null)
    {
        $path = $this->locator->locate($resource);
        $config = ['blog' => ['user_blogs' => $this->loadFile($path)]];

        if (null === $config) {
            return;
        }

        $configuration = new BlogConfiguration();
        $processor = new Processor();
        $processedConfig = $processor->processConfiguration(
            $configuration,
            $config
        );

        return $processedConfig;
    }

    public function supports($resource, $type = null)
    {
        return is_string($resource) && 'json' === pathinfo(
            $resource,
            PATHINFO_EXTENSION
        );
    }

    private function loadFile($path)
    {
        return json_decode(file_get_contents($path), true);
    }
}

これで準備完了です。このDSLを元に設定を読み込んでみましょう(bin/test_json.php)。

<?php
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Loader\DelegatingLoader;
use Symfony\Component\Config\Loader\LoaderResolver;

use Example\Config\JsonFileLoader;

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

$locator = new FileLocator(__DIR__.'/../config');

$loaderResolver = new LoaderResolver([new JsonFileLoader($locator)]);
$delegatingLoader = new DelegatingLoader($loaderResolver);

try {
    $config = $delegatingLoader->load('config.json');
    var_dump($config);
} catch (InvalidConfigurationException $e)
{
    echo "コンフィギュレーション構文エラー".PHP_EOL;
    echo $e->getMessage().PHP_EOL;
}

実行結果

$ php bin/test_json.php
array(1) {
  'user_blogs' =>
  array(3) {
    [0] =>
    array(3) {
      'blog_url' =>
      string(32) "http://shibayu36.hatenablog.com/"
      'permission' =>
      string(6) "public"
      'can_be_edited_by' =>
      array(1) {
        [0] =>
        string(10) "shiba_yu36"
      }
    }
    [1] =>
    array(3) {
      'blog_url' =>
      string(40) "http://shibayu36-private.hatenablog.com/"
      'permission' =>
      string(7) "private"
      'can_be_edited_by' =>
      array(2) {
        [0] =>
        string(10) "shiba_yu36"
        [1] =>
        string(10) "shiba_yu37"
      }
    }
    [2] =>
    array(3) {
      'blog_url' =>
      string(24) "http://blog.example.com/"
      'permission' =>
      string(6) "public"
      'can_be_edited_by' =>
      array(1) {
        [0] =>
        string(12) "example-user"
      }
    }
  }
}

コンフィギュレーションのダンプ機能を使えば、infoやisRequiredの情報は出力できます(YamlDumperかXmlDumperしかないので、以下の例はYamlDumperで出力しています)。

$ php bin/dump.php
blog:
    user_blogs:           # Required

        # 設定したいブログのURL
        blog_url:             ~ # Required

        # publicなら公開、privateなら非公開
        permission:           ~ # One of "public"; "private", Required

        # 編集権限を持つユーザ
        can_be_edited_by:     [] # Required

今回のコンフィギュレーションでは、次のようなルールも組み込んであります。

  • blog_urlには、URL形式の文字列
  • permissionには、publicかprivate

設定に想定外の文字列があると、読み込み時に次のようにエラーを検出できます。

$ php bin/test_json.php
コンフィギュレーション構文エラー
The value "hogehoge" is not allowed for path "blog.user_blogs.0.permission". Permissible values: "public", "private"

グラマー定義は設定ファイルのフォーマットには依存していないため、次のようにYAML用のローダーを用意すれば、同じDSLをYAMLで記述して読み込むこともできます(src/YamlFileLoader.php bin/test_yaml.php)。

-
    blog_url: http://shibayu36.hatenablog.com/
    permission: public
    can_be_edited_by: [ shiba_yu36 ]
-
    blog_url: http://shibayu36-private.hatenablog.com/
    permission: private
    can_be_edited_by: [ shiba_yu36, shiba_yu37 ]
-
    blog_url: http://blog.example.com/
    permission: public
    can_be_edited_by: [ example-user ]

まとめ

設定の意図は問題空間の言語であり、DSLとして表現することでその意図を明確に表せるのと同時に、内部の実装と切り離すことができます。実際のアプリケーションでは、DSLスクリプトである設定ファイルを読み込んだ後、DIコンテナ等と連携しながら内部(解決)モデルの構成処理などを行います。リポジトリとして扱いたいといった解決のための要請は、この段階で組み込んでいくわけです。


参考

configuration dsl symfony generative.programming

Practical Symfony #23: ドメインの知識を使ったフォームバリデーション

フォームは、PHPメンターズの設計と実装の型で述べているように、アプリケーションレイヤーにて実装されます。今回はフォームのバリデーションの拡張についてとりあげます。

バリデーションの仕組みの基本

ユーザーが入力した値を受け取り、アプリケーションのフォームでその入力を表すオブジェクト(フォームのデータを格納する入れ物、フォームDTO: Data Transfer Objectと名づけます)が組み立てられます。このフォームDTOの持つデータが妥当かどうかをチェックするのがバリデーションの役割です。

image

バリデーションはフォームDTOに対して行われるため、SymfonyではフォームDTOクラスにバリデーションの定義を記述します。

class Author
{
    /**
     * @Assert\NotBlank()
     * @Assert\Length(min = "3")
     */
    private $firstName;
}

フォームDTOのデータを確認するための様々な制約がSymfonyに用意されています。基本的には、フォームDTOのフィールドについて検証を行うもので、処理はフォームDTO内に閉じている前提になっています。

しかし、フォームを使うアプリケーションでは単純に単一エントリのデータの妥当性検査を行うだけでなく、関連データも含めた整合性検査も行いたい場合があります。簡単な例は「登録するメールアドレスがシステム内でユニークであること」といった条件です。ユーザーが入力したデータだけでなく、すでに登録されているデータを検索してチェックしなくてはなりません。Symfonyにはこの目的に特化したUniqueEntityという制約が用意されていますが、このような処理を一般化して考えると、単なるユニーク制約ではなく、「ドメインのルールに基づくバリデーション」を行いたいということになります。


ドメインのルールはどこに表されている?

ドメイン駆動設計に「仕様(Specification)」というパターンがあります(Symfonyでの実装例)。この名前から想起されるとおり、バリデーションで使いたいようなルールは、ドメインの仕様として表すことができます。先ほど例に挙げたユニークであるかどうかという制約もドメインにおける仕様です。仕様オブジェクトとして独立させる他に、リポジトリのメソッドとして表現してもよいでしょう。

image

仕様として表現する場合、これは1つのオブジェクトで、状態を持たないサービスの一種です。この中では自由に他のサービスやリポジトリを組み合わせて使えます。ですから、アプリケーションのフォームにおけるバリデーションに、ドメインレイヤーのサービスを手軽に指定できればよさそうです。


バリデーションにサービスのメソッドを使う

Alert  これ以降の「サービス」は、DDDのサービスのことではなくて、Symfonyのコンテナで扱うサービスを指します。

Symfonyはサービスコンテナをアーキテクチャの基盤に持っており、さまざまな場面でサービスに処理を分散させることができます。ドメインレイヤーのサービスでも、コンテナに登録してあればどこからでも呼び出せます。

image

問題は、サービスを使ったバリデーションのための制約が、Symfonyの組み込みでは用意されていないということです。また、制約を記述しているフォームDTOやエンティティは、サービスコンテナの参照を保持しません。どうやってサービスをバリデーション時に呼び出せばよいでしょうか?

筆者の使っているServiceCallbackという制約を紹介します。ServiceCallback制約を使うと、フォームDTOに対してサービスのメソッドをバリデーションに使えます。以下は、サービスコンテナに登録されたdomain.member.allow_upgrade_specというサービスのisSatisfiedByメソッドをバリデーション時に呼び出す記述例です。

/**
 * @AssertServiceCallback(service="domain.member.allow_upgrade_spec",
 method="isSatisfiedBy", message="you can't upgrade.")
 */
class Member
{

呼び出すサービスの方はとてもシンプルで、フォームDTOを引数で受け取るisSatisfiedByを定義し、サービスとして登録するのみです。必要であれば他のサービスやリポジトリなどをDIで注入して使うこともできます。検査結果をtrue/falseで返します。

use JMS\DiExtraBundle\Annotation As DI;

/**
 * @DI\Service("domain.member.allow_upgrade_spec")
 */
class AllowUpgradeSpecification
{
    /**
     * inject services if you need
     */
    private $memberRepository;

    public function isSatisfiedBy($member)
    {
        // リポジトリなどを使った条件のチェック
        ...

        return true;
    }
}

ServiceCallbackバリデーターの実装

カスタムバリデーターの実装には、ServiceCallback制約とServiceCallbackValidatorを作り、バリデーターとして使えるようサービスコンテナに登録しています。

<?php
namespace PHPMentors\ValidatorBundle\Validator\Constraints;

use Symfony\Component\Validator\Constraint;

/**
 * @Annotation
 */
class ServiceCallback extends Constraint
{
    public $method;
    public $service;
    public $message = 'Service callback returns an error.';

    /**
     * {@inheritdoc}
     */
    public function getTargets()
    {
        return self::CLASS_CONSTRAINT;
    }

    /**
     * {@inheritdoc}
     */
    public function validatedBy()
    {
        return 'PHPMentorsServiceCallbackValidator';
    }
}
<?php
namespace PHPMentors\ValidatorBundle\Validator\Constraints;

use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;

class ServiceCallbackValidator extends ConstraintValidator implements ContainerAwareInterface
{
    /**
     * @var ContainerInterface
     */
    protected $container;

    /**
     * {@inheritdoc}
     */
    public function validate($object, Constraint $constraint)
    {
        if (null === $object) {
            return;
        }

        if (!$this->container->has($constraint->service)) {
            throw new ConstraintDefinitionException;
        }

        $service = $this->container->get($constraint->service);

        if (!method_exists($service, $constraint->method)) {
            throw new ConstraintDefinitionException(sprintf('Method "%s" targeted by ServiceCallback constraint does not exist', $constraint->method));
        }

        $result = call_user_func(array($service, $constraint->method), $object);

        if (false == $result) {
            $this->context->addViolation($constraint->message);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function setContainer(ContainerInterface $container = null)
    {
        $this->container = $container;
    }
}
parameters:
    php_mentors_validator.service_callback.class: PHPMentors\ValidatorBundle\Validator\Constraints\ServiceCallbackValidator

services:
    php_mentors_validator.service_callback:
        class: %php_mentors_validator.service_callback.class%
        calls:
            - [ setContainer, ["@service_container"] ]
        tags:
            - { name: validator.constraint_validator, alias: PHPMentorsServiceCallbackValidator }

上記コードはSymfony 2.4で動作確認しているものです。2.0向けでは多少書き換えないといけません。動作しているコード例をこちらにあげてあります。

まとめ

ServiceCallback制約を使うことで、バリデーションの記述の自由度が増し、バリデーションのためにフォームDTOやエンティティに余計な知識を埋め込んでしまうことを避けられます。同時に、ドメインの知識がアプリケーションに散らばってしまうことをも防げます。

この制約の実装自体が(厳密なエラー処理等をしていないことを除外しても)とてもシンプルなのは、サービスコンテナやバンドルを土台としているSymfonyのアーキテクチャの恩恵です。しかしその一方で、今回紹介したようにアプリケーションを開発する上でSymfonyに足りない要素があることも事実です。少しだけ手間をかけて仕組みを用意することで、アプリケーションレイヤーとドメインレイヤーの分離を維持することができます。こうしてSymfonyのようなOSSのフレームワークを自分用に育てていけば、頑強な基盤と同時に高い生産性を無理なく両立していけるでしょう。

参考

ddd validation form practical.symfony symfony

Symfony Meetup Tokyo での Extract Till You Drop の写経

PHPメンターズ道場生の @ganchiku です。よろしくお願いします。

はじめに

10月4日 Engine Yard 東京オフィスにて、Symfony Meetup が開催され、14人ほどの参加者がありました。そこでのテーマは、Symfony Live London 2013 のセッションのうち Mathias Verraes さんの Extract Till You Drop(極限まで抽出せよ)のコードを真似てみよう、というものでした。Extract Till You Drop という言葉は、Uncle Bob ことロバート・マーチン氏の引用になります。

さて、Mathias さんのライブコーディングは、 YouTube にアップロードされており、その過程を PHP メンターズの後藤さんが説明しながら、一緒に写経を行いました。

http://verraes.net/2013/09/extract-till-you-drop/

モデリング、ユースケース

このプログラムの扱うケースはシンプルで、「1グループ3人までの児童をグループを作る」といったケースになります。モデリングをすると、以下のような内容になります。

エンティティ

  • 児童(Pupil)
  • グループ(Group)

リポジトリ

  • 児童リポジトリ(PupilRepository)
  • グループリポジトリ(GroupRepository)

サービス

  • グループ(Group)に児童(Pupil)を加えるサービス(GroupService のメソッド)

ドメインユースケース

  • グループに児童を加えるサービスが、グループIDと児童IDを受け取る。
  • グループID、児童IDを元にそれぞれのリポジトリから該当するエンティティを取り出す。
  • グループエンティティへ児童エンティティを追加する。
  • 追加を行う際に、追加をしようとするグループに3人より多く児童を入れることはできない。
  • 追加を行う際に、追加をしようとするグループに重複して同じ児童を入れることはできない。

実践

これらの処理は既にプログラムとして作成されているのですが、コードが散らかっているのでリファクタリングしていこうといったものです。その際に、 PhpStorm を使い、リファクタリングやテストの説明をしていってくれます。

リファクタリング以前の最初のソースコードは、次にあります。

https://github.com/mathiasverraes/extract-till-you-drop

1.変数名やメソッド名を明確にしていきます

  • $repository という変数は、曖昧なため、何のリポジトリか明確にします。
  • $id という変数は、曖昧なため、何の ID か明確にします。

などなど。

確かに、変数名が長いのは好ましくはないのですが、この時点では、変数名に曖昧性を持たせずに、意味のあるものに全部変更していきます。

PhpStorm では、リファクタリングの機能があり、変数名の変更が容易にできます。

変数名の変更の図

image

image
使用したショートカット:

Ctrl+t : 変数名やメソッドの変更(rename)に使用する。

$repository -> $groupRepository

$id -> $groupId

function add -> function enlistPupilInGroup

$pupils -> $pupilsInGroup

$addPupil -> $pupilToBeEnlisted

$pupil -> $pupilInGroup

$tmp -> $pupilAlreadyInGroup

2.モックを使用し、テストを書きます

  • GroupRepository の find の返り値などをモックを使用してテストを行います。
  • その際にテストの中でもモックを作成するところに共通の処理があるため、テスト内でメソッドとして抽出していきます。
テストのカバレッジを色付け

PHPStorm では、テストのカバレッジのテストが通った場所に色を付けることができ、全て通るようにしていきます。テストの共通部分の抽出も簡単にできます。

Preference で IDE Settings -> Editor -> Colors & Font -> General で新しくscheme name を作成して色付けを行う。

Full line coverage の Background を緑に

image

Uncovered line の Background を赤に

image

Test with coverage をすると、通った場所は緑に、通っていない場所は赤で表示される。

image
モック作成部をメソッドへ抽出

image
テストは、日本語で書くことが可能

image

使用したショートカット:

Shift+Cmd+t : 新しくテストを作成するときに使用する。

GroupServiceTest.php の作成する。

Cmd+o : クラス名を指定して開く(オートコンプリート便利)。

GroupServiceTest.php を開く。

Ctrl-t : ネームスペースの変更(ファイルの移動)に使用する。

School -> School\Tests

Cmd+n: 便利な生成

PHPUnitテストメソッド生成する。

PHPDoc ブロック生成する。

メソッドのオーバーライドで setUp を生成する。

Option+Return: ヒントの表示からクラスのインポートをする。

GroupService

GroupRepository

PupilRepository

Option+Return: ヒントの表示からフィールドを追加する。

$this->groupRepository

$this->pupilRepository

$this->SUT

$this->group

$this->pupil

Cmd + d: 行を複製する。

        $this->groupRepository = $this->getMock(‘School\GroupRepository’);

を複製し、

$this->pupilRepository = $this->getMock(‘School\PupilRepository’);

を簡単に作る

Ctrl + t : 定数を抽出

ハードコードの数値を定数GROUPIDに抽出

ハードコードの数値を定数PUPILIDに抽出

まとまりのある行を選択して、 Ctrl + t : まとまりをメソッドに抽出する

GroupRepository の find メソッドのモック作成を private メソッドに抽出する。

PupilRepository の find メソッドのモック作成を private メソッドに抽出する。

GroupRepository の persist メソッドのモック作成を private メソッドに抽出する。

3.カバレッジが 100% になったら、リファクタリングを開始します

  • 処理をわかりやすくするため、ネストを浅くします。テストが通るか調べます。
  • guard メソッドを抽出する。テストが通るか調べます。
  • guard メソッドは、GroupServiceクラスよりも、Groupクラスにある方が自然なので、そちらに移動する。テストが通るか調べます。
変数をインラインに抽出する

image
メソッドへの抽出

image
メソッドの引数を操作する

image
使用したショートカット:

選択して Ctrl+Option+i : オートインデントをする

ネストレベルをリファクタリングしたときに使用

Ctrl + t : 変数をインラインに変更する

$pupilsInGroup を $group->getPupils() に変更

選択して Ctrl+t : メソッドを抽出

3人以上追加できないルールをメソッドへ抽出 guardAgainstTooManyPupils

同じ児童を追加できないルールをメソッドへ抽出 guardAgainstDuplicatedPupils

Ctrl+t : 変数の名前を簡潔にする

$pupilToBeEnlisted を $pupil に名前変更

Ctrl+t : メソッドの引数の変更

guardAgainstDuplicatedPupils の引数から $group を削除し、PHPDoc にも反映する。

ライブコーディングでは、ここまでなのですが、guardメソッドがGroupServiceクラスからGroupクラスに移動されたため、GroupTestを作成し、そちらに書く方が良い、といって締めくくっています。

感想

リファクタリングをしていく際に、「Explict(明確にする)」という言葉を多用していたように、単純にコードを整理していくのではなく、より意味のわかるコードに抽出していくことに注目していたと思います。

また、私も PHPStorm を使い始めたばかりなのですが、スムーズにリファクタリングしていくための IDE としてとても便利だな、と思い始めています。まだまだショートカットに慣れていなくてぎこちないのですが、このまま使っていく予定です。

symfony dojo refactoring ide phpstorm

Practical Symfony #22: 出力バッファはどこで送信されるのか?

PHPの出力バッファリングは、日本において文字エンコーディングの変換を中心に広く使われてきました。文字エンコーディングとしてUTF-8が定着してきた現在、出力バッファリングは出力文字エンコーディングの変換という用途では余り使われなくなってきていると思われますが、古いアプリケーションでは依然として必要とされていることでしょう。

以下のコードは出力バッファリングの簡単な例です。

<?php
ob_start(function ($buffer) {
    return '__START__' . $buffer . '__END__';
});

echo 'foo';

ob_end_flush();

echo 'bar';

// "__START__foo__END__bar" と表示される

出力バッファの内容は、ob_end_flush()関数やob_get_flush()関数の呼び出しによって明示的に出力バッファをフラッシュするか、スクリプトが終了するタイミングで標準出力に送信されます。

Symfonyにおける出力バッファの扱い

SymfonyではSymfony\Component\HttpFoundation\Response::send()メソッドによってHTTPヘッダと内容の送信が行われます。

Symfony\Component\HttpFoundation\Response(v2.3.4):

<?php
...
class Response
{
    ...
    /**
     * Sends HTTP headers and content.
     *
     * @return Response
     *
     * @api
     */
    public function send()
    {
        $this->sendHeaders();
        $this->sendContent();

        if (function_exists('fastcgi_finish_request')) {
            fastcgi_finish_request();
        } elseif ('cli' !== PHP_SAPI) {
            // ob_get_level() never returns 0 on some Windows configurations, so if
            // the level is the same two times in a row, the loop should be stopped.
            $previous = null;
            $obStatus = ob_get_status(1);
            while (($level = ob_get_level()) > 0 && $level !== $previous) {
                $previous = $level;
                if ($obStatus[$level - 1]) {
                    if (version_compare(PHP_VERSION, '5.4', '>=')) {
                        if (isset($obStatus[$level - 1]['flags']) && ($obStatus[$level - 1]['flags'] & PHP_OUTPUT_HANDLER_REMOVABLE)) {
                            ob_end_flush();
                        }
                    } else {
                        if (isset($obStatus[$level - 1]['del']) && $obStatus[$level - 1]['del']) {
                            ob_end_flush();
                        }
                    }
                }
            }
            flush();
        }

        return $this;
    }
    ...

出力バッファの送信は、HTTPヘッダと内容の送信の後の少々込み入ったコードで実装されています。コードを見ると、出力バッファのうち削除フラグがtrueのものだけが実際に送信されることがわかります。

Symfonyにはob_start()関数相当のAPIはありません。また、ob_end_clean()関数による出力バッファのクリアも行われません。よってSymfonyでは設定ファイル等による実行時設定やユーザーによるob_start()関数の呼び出しを通した出力バッファリング設定が、そのまま実行環境で有効となります。

出力バッファリングを使う場合はバージョン2.3.4以降または2.2.6以降を使うこと

Symfony 2.3.3以前および2.2.6以前のバージョンは、PHP 5.4で導入された新しい出力APIをサポートしていないため、前述のコードによる出力バッファのクリアが行われません。出力バッファリングを使う場合は、バージョン2.3.4以降または2.2.6以降を使うようにしましょう。

参考

practical.symfony symfony