PHPerのための「PHPフレームワーク」を語り合うPHP TechCafe

https://rakus.connpass.com/event/264108/

PHPer's NEWS

PHP8.2のリリースがちょっと延期
バグが見つかったんなら仕方がないね

The PHP Foundation: Impact and Transparency Report 2022
PHPの保守・開発は順調に進んでいるようでなによりです。

特集:「PHPフレームワーク」を語り合う

  • Webアプリケーションフレームワーク とは(wiki)

    • 動的な ウェブサイト、Webアプリケーション、Webサービスの開発をサポートするために設計されたアプリケーションフレームワーク
      • Web開発で用いられる共通した作業に伴う労力を軽減する
        • データベースへのアクセス
        • テンプレートエンジン
        • セッション管理
          • コードの再利用を促進させる
  • そもそもFWってなんで必要?

    • 開発速度向上
      • Webアプリケーション開発でよく利用する処理(セッション管理やDBアクセス、Cookieなど)が既に用意されているため、それらを再利用するだけで開発が進められる
    • セキュリティ対応
      • 脆弱性が見つかった場合に修正版がリリースされる
        • メンテされているOSSの場合
    • 開発ルールの順守
      • フレームワークのルールに従って作成することが強いられる反面、開発チーム全体で共通のルールで開発できるため、ルールに逸脱するようなコードが生まれにくい

比較するフレームワーク

  • Laravel
  • Symfony
  • CakePHP
  • Slim

設計思想

Laravel

https://laravel.com/docs/9.x/installation

  • プログレッシブフレームワーク
    • we mean that Laravel grows with you.
    • Laravel はあなたと共に成長するということです。
    • Laravel は依存性注入、単体テスト、キュー、リアルタイム イベント などのための堅牢なツールを提供します。
  • スケーラブルなフレームワーク
    • Laravel は信じられないほどスケーラブルです。
    • 実際、Laravel アプリケーションは、1 か月あたり数億のリクエストを処理するように簡単にスケーリングされています。
  • コミュニティ フレームワーク

Symfony

What could be more useful than an application developed by users for their own needs
https://symfony.com/at-a-glance#a-philosophy

ユーザーが自分たちのニーズに合わせて開発したアプリケーションほど便利なものはない

作成されたコンポーネントを組み合わせて、フルスタックフレームワークを作成することもマイクロサービスを作成することも可能。
開発者の目的に応じて規模を変えることができることが特徴。
コンポーネントが標準化されており、アプリケーションが成熟しても使用したいコンポーネントを自由に導入することができる。

Java の Spring Framework や Ruby の Ruby on Rails の影響を受けているとのこと。

Symfony コンポーネントは Drupal, Prestashop, Laravel で利用されている

CakePHP

  • MVC

公式の図

Slim

Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs.
https://www.slimframework.com/

Slimはシンプルかつ強力な Web アプリケーションと API をすばやく作成するのに役立つ PHP マイクロ フレームワークです。

At its core, Slim is a dispatcher that receives an HTTP request, invokes an appropriate callback routine, and returns an HTTP response. That’s it.
You don’t always need a kitchen-sink solution like Symfony or Laravel. These are great tools, for sure. But they are often overkill. Instead, Slim provides only a minimal set of tools that do what you need and nothing else.
https://www.slimframework.com/docs/v4/

本質的に、Slim は HTTP リクエストを受け取り、適切なコールバック ルーチンを呼び出し、HTTP レスポンスを返すディスパッチャーです。それだけ。
SymfonyやLaravelのようなキッチン シンク ソリューションが常に必要なわけではありません。これらは確かに優れたツールです。しかし、それらはしばしばやり過ぎです。代わりに、Slim は、必要なことだけを行う最小限のツール セットのみを提供します。

ルーティング

Laravel

https://laravel.com/docs/9.x/routing

デフォルトルートファイル

デフォルトでは下記2つファイルにルーティングを定義する

  • routes/web.php
  • routes/api.php

定義方法

UserControllerにindxメソッドを定義している場合
下記のように定義すると/userのパスに対して、UserControllerのindexメソッドが対応される

<?php use App\Http\Controllers\UserController; Route::get('/user', [UserController::class, 'index']);

利用可能なルーターメソッド

<?php Route::get($uri, $callback); Route::post($uri, $callback); Route::put($uri, $callback); Route::patch($uri, $callback); Route::delete($uri, $callback); Route::options($uri, $callback);

パラメータ

<?php Route::get('/posts/{post}/comments/{comment}', [CommentController::class, 'show']);

Symfony

routes.yaml に記載するパターン

routes.yaml を以下の通り編集する

app_lucky_number: path: /lucky/number controller: App\Controller\LuckyController::number

/lucky/number にアクセスすることで LuckyControllernumber メソッドにルーティングされる

アノテーションまたはアトリビュートを利用するパターン(こっちが推奨)

コントローラを以下の通り変更

<?php // src/Controller/LuckyController.php namespace App\Controller; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; class LuckyController { #[Route('/lucky/number')] public function number(): Response { $number = random_int(0, 100); return new Response( '<html><body>Lucky number: '.$number.'</body></html>' ); } }

上記の通り記述することで routes.yaml を作成しなくともルーティングされる

CakePHP

https://book.cakephp.org/3/ja/development/routing.html

routes.phpに記載

例:/にアクセスするとArticlesControllerindex()メソッドを実行する

use Cake\Routing\Router; // スコープ付きルートビルダーを使用。 Router::scope('/', function ($routes) { $routes->connect('/', ['controller' => 'Articles', 'action' => 'index']); }); // static メソッドを使用。 Router::connect('/', ['controller' => 'Articles', 'action' => 'index']);

/articles/15にアクセスするとArticlesControllerview(15)メソッドを実行する

$routes->connect( '/articles/:id', ['controller' => 'Articles', 'action' => 'view'] ) ->setPatterns(['id' => '\d+']) ->setPass(['id']);

HTTPメソッドによって分けたいときは以下の記述

// GET リクエストへのみ応答するルートの作成 $routes->get( '/cooks/:id', ['controller' => 'Users', 'action' => 'view'], 'users:view' ); // PUT リクエストへのみ応答するルートの作成 $routes->put( '/cooks/:id', ['controller' => 'Users', 'action' => 'update'], 'users:update' );

Slim

https://www.slimframework.com/docs/v4/objects/routing.html

$app->get('/books/{id}', function ($request, $response, array $args) { // Show book identified by $args['id'] });

セッション管理

Laravel

https://laravel.com/docs/9.x/session

設定ファイル

config/session.php

セッションの操作方法

グローバルセッションヘルパーとRequestインスタンス経由の2つの方法

グローバルセッションヘルパー
<?php $value = session('key');
Requestインスタンス経由
<?php public function show(Request $request, $id) { $value = $request->session()->get('key'); // }

Symfony

以下の2通り

  • RequestStack から取得
  • SessionInterface から取得

RequestStack から取得するパターン

  • HttpFoundation component を追加することで利用可能
composer require symfony/http-foundation
  • セッションの設定は config/packages/framework.yaml に記載
    • 公式にはXMLとPHPの説明もあるが、デフォルトは yaml
  • handler_id
    • null
      • 稼働しているPHPの設定に依存
    • session.handler.native_file
      • save_path に記載したパスにSymfonyのセッションメタデータを保存
    • その他詳細
基本的な使い方
use Symfony\Component\HttpFoundation\RequestStack; class SomeService { private $requestStack; public function __construct(RequestStack $requestStack) { $this->requestStack = $requestStack; // Accessing the session in the constructor is *NOT* recommended, since // it might not be accessible yet or lead to unwanted side-effects // $this->session = $requestStack->getSession(); } public function someMethod() { $session = $this->requestStack->getSession(); // stores an attribute in the session for later reuse $session->set('attribute-name', 'attribute-value'); // gets an attribute by name $foo = $session->get('foo'); // the second argument is the value returned when the attribute doesn't exist $filters = $session->get('filters', []); // ... } }
  • 公式にはコンストラクタ内でセッションを利用することはおすすめしていない。
  • Symfony 6.0 より、旧方式のセッションは廃止された模様
SessionInterface から取得するパターン

SessionInterface でタイプヒントしてコントローラ引数に渡すだけ。

基本的な使い方
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Session\SessionInterface; // ... public function index(SessionInterface $session): Response { // stores an attribute for reuse during a later user request $session->set('foo', 'bar'); // gets the attribute set by another controller in another request $foobar = $session->get('foobar'); // uses a default value if the attribute doesn't exist $filters = $session->get('filters', []); // ... }

CakePHP

設定

データベースセッション
'Session' => [
    'defaults' => 'database',
    'handler' => [
        'engine' => 'DatabaseSession',
        'model' => 'CustomSessions'
    ]
]
  • 1行目:cakePHPにセッションはデータベースに保持することを伝える
  • セッション保持するためのカスタムモデルを定義することもできる(model => 'CustomSessions')
キャッシュセッション
Configure::write('Session', [
    'defaults' => 'cache',
    'handler' => [
        'config' => 'session'
    ]
]);
  • これは Session に CacheSession クラスをセッション保存先として 委任する
セッションを利用する
  • セッションはリクエストオブジェクトを呼び出せる場所ならどこでも呼び出せる
    • Controllers
    • Views
    • Helpers
    • Cells
    • Components
$name = $this->getRequest()->getSession()->read('User.name');

// 複数回セッションにアクセスする場合、
// ローカル変数にしたくなるでしょう。
$session = $this->getRequest()->getSession();
$name = $session->read('User.name');
  • Session::read($key)
  • Session::write($key)
  • Session::check($key)
  • Session::destroy()
  • ユーザーがログインやログアウトした時、 AuthComponent は自動的にセッション ID を更新

Slim

実装なし

リクエスト管理

Laravel

https://laravel.com/docs/9.x/requests

リクエストの内容を取得する

Illuminate\Http\Requestクラスのインスタンスを生成して取得する

<?php $name = $request->input('name'); $name = $request->query('name');

Symfony

https://symfony.com/doc/current/components/http_kernel.html

use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; public function index(Request $request): Response { $request->isXmlHttpRequest(); // is it an Ajax request? $request->getPreferredLanguage(['en', 'fr']); // retrieves GET and POST variables respectively $request->query->get('page'); $request->request->get('page'); // retrieves SERVER variables $request->server->get('HTTP_HOST'); // retrieves an instance of UploadedFile identified by foo $request->files->get('foo'); // retrieves a COOKIE value $request->cookies->get('PHPSESSID'); // retrieves an HTTP request header, with normalized, lowercase keys $request->headers->get('host'); $request->headers->get('content-type'); }

CakePHP

$this->request を使用して取得

$controllerName = $this->request->getParam('controller'); // URL は /posts/index?page=1&sort=title の場合に page を取得するとき $page = $this->request->getQuery('page'); // POSTデータにアクセスするとき $title = $this->request->getData('MyModel.title');

Slim

https://www.slimframework.com/docs/v4/objects/request.html

<?php use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Slim\Factory\AppFactory; require __DIR__ . '/../vendor/autoload.php'; $app = AppFactory::create(); $app->get('/hello', function (Request $request, Response $response) { $response->getBody()->write('Hello World'); return $response; }); $app->run();

エラーハンドリング

Laravel

https://readouble.com/laravel/9.x/ja/errors.html

  • App\Exceptions\Handlerクラスによって、アプリケーションが投げるすべての例外がログに記録され、ユーザーへレンダーされる

エラーハンドリングのカスタマイズ

  • Handlerクラスは、カスタム例外レポートとレンダリングコールバックを登録できるregisterメソッドを持っている。

    • reportableメソッドで、例外をさまざまな方法で報告できる。(エラー監視ツールに登録するなど。デフォルトではログに記録される。)

    • renderableメソッドで、特定の例外に対して、個別にレンダリング方法を指定することができる。(デフォルトでは例外はHTTPレスポンスに変換される)

      • HTTPエラーが返された場合、resources/views/errors下のHTTPステータスコード名のbladeファイルがレンダリングされる(404.blade.phpなど)
use App\Exceptions\InvalidOrderException; /** * アプリケーションの例外処理コールバックを登録 * * @return void */ public function register() { $this->reportable(function (InvalidOrderException $e) { // 例外を報告 }); $this->renderable(function (InvalidOrderException $e, $request) { return response()->view('errors.invalid-order', [], 500); }); }

Symfony

Symfony アプリケーションでは、エラーが 404 Not Found エラーであろうと、
コードで何らかの例外をスローすることによってトリガーされた致命的なエラーであろうと、
すべてのエラーを例外として扱う。

組み込みの Twig エラーレンダラーを使用して、デフォルトのエラーテンプレートをオーバーライド可能。

composer require symfony/twig-pack

これらのテンプレートをオーバーライドするには、標準の Symfony メソッドを使用し て、バンドル内にあるテンプレートをオーバーライドし、それらをtemplates/bundles/TwigBundle/Exception/ディレクトリに配置する。

HTML ページを返す典型的なプロジェクトは次のようになります。

 Copy
templates/
└─ bundles/
   └─ TwigBundle/
      └─ Exception/
         ├─ error404.html.twig
         ├─ error403.html.twig
         └─ error.html.twig      # All other HTML errors (including 500)

HTML ページの 404 エラー テンプレートをオーバーライドするには、次の場所にある新しい error404.html.twigテンプレートを作成しtemplates/bundles/TwigBundle/Exception/ます。

{# templates/bundles/TwigBundle/Exception/error404.html.twig #}
{% extends 'base.html.twig' %}

{% block body %}
    <h1>Page not found</h1>

    <p>
        The requested page couldn't be located. Checkout for any URL
        misspelling or <a href="{{ path('homepage') }}">return to the homepage</a>.
    </p>
{% endblock %}

error_controller
参考

CakePHP

https://book.cakephp.org/3/ja/development/errors.html

ErrorController にてエラーページを描画

namespace App\Controller\Admin; use App\Controller\AppController; use Cake\Event\EventInterface; class ErrorController extends AppController { /** * Initialization hook method. * * @return void */ public function initialize(): void { $this->loadComponent('RequestHandler'); } /** * beforeRender callback. * * @param \Cake\Event\EventInterface $event Event. * @return void */ public function beforeRender(EventInterface $event) { $this->viewBuilder()->setTemplatePath('Error'); } }

Slim

https://www.slimframework.com/docs/v4/middleware/error-handling.html

use Slim\Factory\AppFactory; require __DIR__ . '/../vendor/autoload.php'; $app = AppFactory::create(); /** * The routing middleware should be added earlier than the ErrorMiddleware * Otherwise exceptions thrown from it will not be handled by the middleware */ $app->addRoutingMiddleware(); /** * Add Error Middleware * * @param bool $displayErrorDetails -> Should be set to false in production * @param bool $logErrors -> Parameter is passed to the default ErrorHandler * @param bool $logErrorDetails -> Display error details in error log * @param LoggerInterface|null $logger -> Optional PSR-3 Logger * * Note: This middleware should be added last. It will not handle any exceptions/errors * for middleware added after it. */ $errorMiddleware = $app->addErrorMiddleware(true, true, true); // ... $app->run();
  • slimが用意したエラー画面を出すかどうかを選択できたり、カスタムエラー画面を表示するなど柔軟な設定が可能。

DBサポート

Laravel

https://readouble.com/laravel/9.x/ja/database.html

コネクション

Lavavelでは以下のDBがサポートされる

  • MariaDB
  • MySQL
  • PostgreSQL
  • SQLite
  • SQL Server

config/database.phpで設定を行う

データベースにアクセスする方法は以下

  • DBファサード
$users = DB::select('select * from users where active = ?', [1]);
  • クエリビルダ
$users = DB::table('users')->where('active', $isActive)->get();
  • Eloquent
$users = User::where('active', $isActive)->get();

Symfony

https://symfony.com/doc/current/doctrine.html

コネクション

  • Doctrine(ORM)を使用
composer require doctrine maker

インストールが完了すると .env ファイルにデータベースへの接続設定に関する項目が書き足されます。
DATABASE_URL の箇所を接続するデータベースに合わせて書き換えて下さい。

DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name
php bin/console doctrine:database:create

エンティティクラスを作成する

symfony console make:entity hoge

CakePHP

SELECT文の実行

use Cake\Datasource\ConnectionManager; $connection = ConnectionManager::get('default'); $results = $connection->execute('SELECT * FROM articles')->fetchAll('assoc');

INSERT文の実行

use Cake\Datasource\ConnectionManager; use DateTime; $connection = ConnectionManager::get('default'); $connection->insert('articles', [ 'title' => 'A New Article', 'created' => new DateTime('now') ], ['created' => 'datetime']);

UPDATE文の実行

use Cake\Datasource\ConnectionManager; $connection = ConnectionManager::get('default'); $connection->update('articles', ['title' => 'New title'], ['id' => 10]);

DELETE文の実行

use Cake\Datasource\ConnectionManager; $connection = ConnectionManager::get('default'); $connection->delete('articles', ['id' => 10]);

Slim

なし

バリデーション

Laravel

https://readouble.com/laravel/9.x/ja/validation.html

複数のバリデーション方法がある
例を2つ紹介

Requestクラスを使う方法

連想配列形式で各パラメータに適応したいバリデーションルールを指定する
他のバリデーションルール

<?php public function rules() { return [ 'title' => 'required|unique:posts|max:255', 'body' => 'required', ]; }

Validatorファサードを使用する方法

ファサードを使ってバリデーションをすることも可能
第一引数に対象のデータ、第二引数にバリデーションルールを指定する

<?php $validator = Validator::make($request->all(), [ 'title' => 'required|unique:posts|max:255', 'body' => 'required', ]);

Symfony

config/validator/ ディレクトリ内の .yaml または .xml ファイルとして定義する。
(例)$nameプロパティが空

Attributes // src/Entity/Author.php namespace App\Entity; // ... use Symfony\Component\Validator\Constraints as Assert; class Author { #[Assert\NotBlank] private $name; }

YAML

# config/validator/validation.yaml App\Entity\Author: properties: name: - NotBlank: ~

XML

<!-- config/validator/validation.xml --> <?xml version="1.0" encoding="UTF-8" ?> <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> <class name="App\Entity\Author"> <property name="name"> <constraint name="NotBlank"/> </property> </class> </constraint-mapping>
PHP // src/Entity/Author.php namespace App\Entity; // ... use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Mapping\ClassMetadata; class Author { private $name; public static function loadValidatorMetadata(ClassMetadata $metadata) { $metadata->addPropertyConstraint('name', new NotBlank()); } }

CakePHP

https://book.cakephp.org/4/en/core-libraries/validation.html#namespace-Cake\Validation

$validator ->requirePresence('title') ->notEmptyString('title', 'このフィールドに入力してください') ->add('title', [ 'length' => [ 'rule' => ['minLength', 10], 'message' => 'タイトルは10文字以上必要です。', ] ]) ->allowEmptyDateTime('published') ->add('published', 'boolean', [ 'rule' => 'boolean' ]) ->requirePresence('body') ->add('body', 'length', [ 'rule' => ['minLength', 50], 'message' => '記事の内容は意味がなければなりません。' ]);

Slim

ドキュメントに記述なし

マイグレーション

Laravel

https://readouble.com/laravel/9.x/ja/migrations.html

  • マイグレージョン関連のコマンドがartisanコマンドで準備されている

    • マイグレーションの生成
    ​​​​php artisan make:migration create_flights_table
    
    • マイグレーションの構造
    ​​​​<?php ​​​​use Illuminate\Database\Migrations\Migration; ​​​​use Illuminate\Database\Schema\Blueprint; ​​​​use Illuminate\Support\Facades\Schema; ​​​​class CreateFlightsTable extends Migration ​​​​{ ​​​​ /** ​​​​ * マイグレーションの実行 ​​​​ * ​​​​ * @return void ​​​​ */ ​​​​ public function up() ​​​​ { ​​​​ Schema::create('flights', function (Blueprint $table) { ​​​​ $table->id(); ​​​​ $table->string('name'); ​​​​ $table->string('airline'); ​​​​ $table->timestamps(); ​​​​ }); ​​​​ } ​​​​ /** ​​​​ * マイグレーションを戻す ​​​​ * ​​​​ * @return void ​​​​ */ ​​​​ public function down() ​​​​ { ​​​​ Schema::drop('flights'); ​​​​ } ​​​​}

Symfony

Doctrine Migrationsを使用。

symfony console make:migration

生成されたファイル名が出力される(migrations/Version20191019083640.php のような名前のファイル):

namespace DoctrineMigrations; use Doctrine\DBAL\Schema\Schema; use Doctrine\Migrations\AbstractMigration; final class Version00000000000000 extends AbstractMigration { public function up(Schema $schema): void { // this up() migration is auto-generated, please modify it to your needs $this->addSql('CREATE SEQUENCE comment_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); $this->addSql('CREATE SEQUENCE conference_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); $this->addSql('CREATE TABLE comment (id INT NOT NULL, conference_id INT NOT NULL, author VARCHAR(255) NOT NULL, text TEXT NOT NULL, email VARCHAR(255) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, photo_filename VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))'); $this->addSql('CREATE INDEX IDX_9474526C604B8382 ON comment (conference_id)'); $this->addSql('CREATE TABLE conference (id INT NOT NULL, city VARCHAR(255) NOT NULL, year VARCHAR(4) NOT NULL, is_international BOOLEAN NOT NULL, PRIMARY KEY(id))'); $this->addSql('ALTER TABLE comment ADD CONSTRAINT FK_9474526C604B8382 FOREIGN KEY (conference_id) REFERENCES conference (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); } public function down(Schema $schema): void { // ... } }

CakePHP

https://book.cakephp.org/migrations/3/ja/index.html

  • Cake PHP 本体には含まれていない
  • Cake PHP コアメンバが開発するプラグインとして利用可能
composer require cakephp/migrations "@stable"
<?php use Migrations\AbstractMigration; class CreateProducts extends AbstractMigration { /** * Change Method. * * More information on this method is available here: * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method * @return void */ public function change() { $table = $this->table('products'); $table->addColumn('name', 'string', [ 'default' => null, 'limit' => 255, 'null' => false, ]); $table->addColumn('description', 'text', [ 'default' => null, 'null' => false, ]); $table->addColumn('created', 'datetime', [ 'default' => null, 'null' => false, ]); $table->addColumn('modified', 'datetime', [ 'default' => null, 'null' => false, ]); $table->create(); } }

* 主キーとなるカラム id は、 勝手に追加される

Slim

なし

Select a repo