読者です 読者をやめる 読者になる 読者になる

「基本からしっかり学ぶ Symfony2 入門」メモ(3)

symfony PHP

「基本からしっかり学ぶ Symfony2 入門」を買った。

基本からしっかり学ぶ Symfony2入門

基本からしっかり学ぶ Symfony2入門

基本からしっかり学ぶ Symfony2入門:書籍案内|技術評論社

hidenorigoto/symfony2-book: 基本からしっかり学ぶSymfony2入門 サポートサイト

Writer::createFromStringの引数について

前回、Write::createFromString('', '')の引数は2つだったと書いたけど、その意味を見ていなかった。第二引数はv7.0では改行コードだったが、v8.0では削除され引数は1つになったようだ。

8-3 独自のサービスの定義

debug:containerでエラーとなった。

> php.exe C:\Users\ishida\PhpstormProjects\classic-symfony\bin\console debug:container app.inquiry_csv_builder


  [Symfony\Component\DependencyInjection\Exception\InvalidArgumentException]
  The file "C:\Users\ishida\PhpstormProjects\classic-symfony\src\AppBundle\De
  pendencyInjection/../Resources/config\services.yml" does not contain valid
  YAML.



  [Symfony\Component\Yaml\Exception\ParseException]
  The reserved indicator "@" cannot start a plain scalar; you need to quote t
  he scalar at line 7 (near "arguments: [%csv_encoding%, @app.inquiry_reposit
  ory]").

'@app.inquiry_repository'とシングルクォートで囲む必要があった。

8-4 独自の設定値の定義

この本の山場であり、Symfonyの最大の特徴ではないかと思う。

MemberjoinedDate@var \DateTimeとなっているがstringなのでは?

prototypeがわからない。

    members:
        山田: {part: トランペット, joinedDate: "2010-10-01"}
        田中: {part: バイオリン, joinedDate: "2008-04-10"}

同じようなものを繰り返す時に使うということか。

例えばこれを

    members:
        - {name: 山田, part: トランペット, joinedDate: "2010-10-01"}
        - {name: 田中, part: バイオリン, joinedDate: "2008-04-10"}

と書くこともできるのだろうか。

できた。

https://github.com/iakio/classic-symfony/commit/d9f4e70820c180996ec9b8704290f450e5893d73

でも一意な識別子がある場合は元の書き方の方が良さそうだ。

「基本からしっかり学ぶ Symfony2 入門」メモ(2)

symfony php

「基本からしっかり学ぶ Symfony2 入門」を買った。

基本からしっかり学ぶ Symfony2入門

基本からしっかり学ぶ Symfony2入門

基本からしっかり学ぶ Symfony2入門:書籍案内|技術評論社

hidenorigoto/symfony2-book: 基本からしっかり学ぶSymfony2入門 サポートサイト

6-2 管理者用画面の実装

「お問い合わせエンティティへの管理フィールドの追加」で、 Inquiryprocess_statusprocess_memoを追加するところでエラーとなった。

> php.exe C:\Users\ishida\PhpstormProjects\classic-symfony\bin\console doctrine:schema:update --force
Updating database schema...

  [Doctrine\DBAL\Exception\NotNullConstraintViolationException]
  An exception occurred while executing 'ALTER TABLE inquiry ADD process_stat
  us VARCHAR(20) NOT NULL':
  SQLSTATE[23502]: Not null violation: 7 ERROR:  列"process_status"にはNULL値
  があります



  [Doctrine\DBAL\Driver\PDOException]
  SQLSTATE[23502]: Not null violation: 7 ERROR:  列"process_status"にはNULL値
  があります



  [PDOException]
  SQLSTATE[23502]: Not null violation: 7 ERROR:  列"process_status"にはNULL値
  があります


doctrine:schema:update [--complete] [--dump-sql] [-f|--force] [--em [EM]] [-h|--help] [-q|--quiet] [-v|vv|vvv|--verbose] [-V|--version] [--ansi] [--no-ansi] [-n|--no-interaction] [-e|--env ENV] [--no-debug] [--] <command>


Process finished with exit code 1 at 21:06:39.
Execution time: 858 ms.

process_status, process_memoはNOT NULLだが、この時点で既にレコードが挿入されていたため。

それぞれデフォルト値を指定することにした。

     * @ORM\Column(name="process_status", type="string", length=20, options={"default":"0"})
...
     * @ORM\Column(name="process_memo", type="text", options={"default":""})

7-1 データフィクスチャ

app/consoleの実行にPhpStormのCommand Line Tool Supportを使っていたのだが、doctrine/doctrine-fixtures-bundleをインストールして新しいコマンドを追加したときは、コマンド一覧のリロードが必要となる。

f:id:iakio:20160220213631p:plain

7-2 CSVダウンロード機能の作成

何故Write::createFromString(",")と引数にカンマを渡しているのだろうと疑問に思っていたのだけれど、サンプルコードを見て意味が分かった。これはダブルクォートじゃなくてシングルクォート2つだった。

つまり、

  • Write::createFromString(",")と、カンマを渡しているように見えたが
  • Write::createFromString('', '')と、空文字を2つ渡していた

買ったのはKindle版。紙の本では見ていない。

7-4 APIの作成

この本は全体的にSymfony以外の部分については、よく言えばシンプルに書いてある。Concertの設計がdate:datetime:timeというのはなかなか大胆。

「ちょっとした確認用にテストコマンドを用意しておくと便利」はとても良いアドバイスだと思う。

「基本からしっかり学ぶ Symfony2 入門」メモ(1)

symfony PHP

「基本からしっかり学ぶ Symfony2 入門」を買った。

基本からしっかり学ぶ Symfony2入門

基本からしっかり学ぶ Symfony2入門

基本からしっかり学ぶ Symfony2入門:書籍案内|技術評論社

hidenorigoto/symfony2-book: 基本からしっかり学ぶSymfony2入門 サポートサイト

環境について

本書での確認環境は、

とのことだが、そのままやってもつまらないので

を使っている。

4-2 フォームの基本

$this->createFormBuilder()->add('name', 'text')は第二引数にクラス名を渡して->add('name', TextType::class)と書くようになったようだ。

choiceの時の引数が変わったようだ。

            ->add('type', ChoiceType::class, [
                'choices' => [
                    '公演について' => 0,
                    'その他' => 1
                ],

4-3 メール通知機能の作成

app/config/parameters.ymlapp/config/parameters.yml.distの違いは?

Gmailを使うのであれば、2段階認証を無効にした開発用メールアドレスを用意するという方法の他に、アプリケーションパスワードを設定しても良さそうだ。

5-4 BlogArticleの作成

レコードの追加時にエラーが発生した。

INSERT INTO blog_article(title, target_date, content) VALUES
  ('本年もよろしくお願い致します', '2015-1-3', '');
[2016-01-25 20:56:06] [23502] ERROR: 列"id"内のNULL値はNOT NULL制約違反です
  詳細: 失敗した行は(null, 本年もよろしくお願い致します, 2015-01-03, )を含みます

blog_articleテーブル定義で、id列にDEFAULT値が設定されていなかった。Doctrine ORMでは、@GeneratedValueのstrategyにAUTO、SEQUENCE、IDENTITYといったものがあり、PostgreSQLを使う場合はAUTOだとこうなる模様。

http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#identifier-generation-strategies

SEQUENCEにしても良さそうだけど、とりあえずデータの投入方法を以下のようにしてお茶を濁した。

INSERT INTO blog_article(id, title, target_date, content) VALUES
  (nextval('blog_article_id_seq'), '本年もよろしくお願い致します', '2015-1-3', '');

2015年はこんな年でした

  • アウトプットを増やさねばと思いつつ、はてなを始めてから一番ブログを書かなかった1年でした。Qiitaはちょっと書いてます
  • OSC2015北海道で発表してきました。多分前にOSCで登壇したのは2010年
  • PhpStorm買いました
  • 将棋ウォーズは去年4級だったけど、今年やっと3級になりました
  • 会社を辞めてフリーランスになりました

とりあえず今年のお気に入りの動画置いておきます

www.nicovideo.jp

www.nicovideo.jp

もっと前はこんな年でした

久しぶりにDockerを動かしてみた(Docker Toolbox)

Docker

前回から2年近くたってるのか。

coreos-vagrantでDockerしてみてわかったこととかハマったこととか - iakioの日記

ホストはWindows10。

インストール

boot2dockerというのを使うんだっけと思って見てみたらDeprecatedと言われたのでDocker Toolboxを使うことにした。

なんとなくKitematicはインストールしなかった。

デスクトップにできたDocker QuickStart Terminalのアイコンを起動してもエラーが出る。VirtualBoxVMはできているがストレージがマウントされていない。どうやらこの辺のバグを踏んだ模様。VirtualBoxを5.0.4にアップデートして解決。

Hello World

ドキュメントがいっぱいある。Hello Worldからみてみる。

$ docker run ubuntu:14.04 /bin/echo 'Hello world'
Error response from daemon: Cannot start container cde072590d243c292d3aee79e0af405d3d7e2ace3be670ebbd6bec9023686b0c: [8] System error: exec: "C:/Program Files/Git/usr/bin/echo": stat C:/Program Files/Git/usr/bin/echo: no such file or directory

これはスラッシュを1つ増やせばいいとどこかで見た。

$ docker run ubuntu:14.04 //bin/echo 'Hello world'
Hello world

しかしbashの方は動かなかった。

$ docker run -t -i ubuntu:14.04 //bin/bash
cannot enable tty mode on non tty input

まあdocker-machine sshすればVirtualBoxの方にはsshできるので、そこから同じことはできる。

$ docker-machine.exe ssh default
...
docker@default:~$  docker run -t -i ubuntu:14.04 /bin/bash
root@83ecba104bec:/#

network

Working with containersをやってみる。

ドキュメントに、boot2dockerを使ってる場合はlocalhostじゃなくてvirtual hostのIP使ってね。 boot2dockerコマンドで調べられるよと書いてるみたいだけど、今回の場合はdocker-machineコマンド のことだと思う。

$ docker-machine.exe ip default
192.168.99.100

取りあえずこの辺まで。

と思ったけどもうちょっと続く。

volume

VirtualBox側には、C:\Usersがそのまま/c/Usersにマウントされている。名前が一緒なのはかなり便利だ。 例えば僕はWindows上のC\Users\ishida\src\github.com\iakio\phpunit-smartrunnerというディレクトリでPHPの何かしらを開発していて、これをテストしたい場合はこのディレクトリで

$ vendor/bin/phpunit tests

と実行している(だいたいGit-Bashを使っている)。これをDockerのコンテナ内で実行したい場合は、PHPがインストールされたコンテナを用意して(phpというそのままの名前のイメージがDocker Hubにある)、同じディレクトリで、

$ docker run -v /$(pwd):/work -w //work php vendor/bin/phpunit tests

としてやればよい。php:7.0などとしてやれば簡単に複数バージョンでのテストもできそうだ。

f:id:iakio:20150923224741p:plain

シンプルなテンプレートエンジンPlatesをSilexで使ってみる

PHP

Platesという、シンプルなテンプレートエンジンを使ってみた。 小さなWebアプリケーションでTwigが必要なほどではない場合にはちょうど良いと思う。

platesphp.com

まずはSilexで最低限のものを実装しよう。

{
    "require": {
        "silex/silex": "~1.3"
    }
}
<?php
// web/index.php
require_once __DIR__ .'/../vendor/autoload.php';

$app = new Silex\Application();

$app->get('/', function () {
    return "Hello";
});
$app->run();

Getting Started

composer.jsonにPlatesを追加してcomposer updateする。

{
    "require": {
        "silex/silex": "~1.3",
        "league/plates": "3.*"
    }
}

では最低限の実装を。

<?php
// web/index.php
require_once __DIR__ .'/../vendor/autoload.php';

$app = new Silex\Application();

$app->get('/', function () {
    $templates = new League\Plates\Engine(__DIR__ . '/../templates');
    return $templates->render('home', ['title' => 'Hello']);
});
$app->run();
<!-- templates/home.php -->
<h1><?=$this->e($title)?></h1>

見ての通り、テンプレートエンジンというよりほぼPHPそのままだ(まあそもそもPHPってそういうものなんだけど)。 PHPにテンプレートエンジンとして使うための便利機能を追加したもの、と考えた方がいいかもしれない。

ルートのアクション内でEngineをnewするのはあんまりなので改善しよう。行儀よくやるならServiceProviderを使うのかもしれないけどここはより単純に、単にコンテナに追加してしまおう。

<?php
// web/index.php
...
$app['view'] = $app->share(function () {
    return new League\Plates\Engine(__DIR__ . '/../templates');
});

$app->get('/', function () use ($app) {
    return $app['view']->render('home', ['title' => 'Hello']);
});

Silexのバージョンは1.3.0で、Pimpleが1.1.1なので$app->share()を使っている。

layoutとinsert

もう少し色々試してみたいんだけど、ネタがないと難しいのでRails Tutorialの静的ページ部分を借りてくることにした。詳細は省くがnpmでbootstrap-sassをインストールし、gruntでビルドできるようにした。

さてレイアウトを導入しよう。

<!-- templates/home.php -->
<?php $this->layout('layouts/application', ['title' => 'Home']) ?>

<div class="center hero-unit">
    <h1>Welcome to the Sample App</h1>

    <h2>
        This is the home page for the
        <a href="http://railstutorial.jp/">Ruby on Rails Tutorial</a>
        sample application.
    </h2>
</div>
<!DOCTYPE html>
<!-- templates/layouts/application.php -->
<html>
<head>
    <title><?=$title?></title>
    <link rel="stylesheet" type="text/css" href="app.css">
    <?=$this->fetch('layouts/shim')?>
</head>
<body>
<?=$this->fetch('layouts/header')?>
<div class="container">
    <?=$this->section('content')?>
    <?=$this->fetch('layouts/footer')?>
</div>
</body>
</html>

$this->layout()でレイアウトを指定し、$this->section()で呼び出している。$this->section()の引数はセクション名で、contentは全ての出力を表す唯一の予約済みのセクション名だ。 明示的にセクションを作る場合は $this->start('name') ... $this->stop() を使う。

$this->layout()でレイアウトを指定すると同時にtitleも渡しているので、index.php側からはtitleを渡す必要は無くなった。

$app->get('/', function () use ($app) {
    return $app['view']->render('home');
});

<?=$this->fetch('layouts/shim')?><?php $this->insert('layouts/shim')?>と書くこともできる。

full_titleヘルパー関数

手順は前後するけど、Rails Tutorialの中にfull_titleというヘルパー関数が出てくるのでこれを実装してみよう。

<?php
// web/index.php
...
$app['view'] = $app->share(function () {
    $engine = new League\Plates\Engine(__DIR__ . '/../templates');
    $engine->registerFunction('full_title', function ($title) {
        $base_title = 'Ruby on Rails Tutorial';
        if (empty($title)) {
            return $base_title;
        }
        return "$base_title | $title";
    });
    return $engine;
});
<!-- templates/layouts/application.php -->
<html>
<head>
    <title><?=$this->full_title($title)?></title>
    <link rel="stylesheet" type="text/css" href="app.css">
    <?=$this->fetch('layouts/shim')?>
</head>
...

以下のようにtitleに空文字以外を渡すと、Ruby on Rails Tutorial | Home と表示され、

<!-- templates/home.php -->
<?php $this->layout('layouts/application', ['title' => 'Home']) ?>

titleに空文字を渡すと、Ruby on Rails Tutorial と表示される。

<!-- templates/home.php -->
<?php $this->layout('layouts/application', ['title' => '']) ?>

ところで、titleはエスケープして出力したほうが良いだろう。つまりfull_titleを通してからescapeすることになる。 これにはいくつかやり方がある。

<?=$this->e($this->full_title($title))?>
<?=$this->batch($title, 'full_title|e')?>
<?=$this->e($title, 'full_title')?>

1つ目は説明するまでもないだろう。2つ目の $this->batch() は、第二引数に複数のヘルパー関数名を | で繋いで記述することができ、 これらが左から順番に実行されるというもの。3つ目も同じような意味で、$this->e() 関数のみ、$this->chan() と同様に 第二引数に複数の関数名を| で繋いで記述することができ、そして最後にエスケープが行われる。

titleのデフォルト値

またちょっと戻るのだけれど、titleを表示したくないときに、

<!-- templates/home.php -->
<?php $this->layout('layouts/application', ['title' => '']) ?>

とやるのはちょっとカッコ悪い。しかしなにも渡さないと Undefined variable になってしまう。 レイアウト側でデフォルトを指定する方法も無くはなさそうだけど不格好なので、 Engine 側でデフォルト値を指定してしまおう。

<?php
// web/index.php
...
$app['view'] = $app->share(function () {
    $engine = new League\Plates\Engine(__DIR__ . '/../templates');
    $engine->addData(['title' => '']);
...

こうすると、全てのテンプレートで title に空文字列がセットされるので、home.php側からはtitleを渡す必要がなくなった。

<!-- templates/home.php -->
<?php $this->layout('layouts/application') ?>