20191201のPHPに関する記事は15件です。

Laravelで超簡単に検索機能を作成するPackageを作成した

この記事はLaravel #2 Advent Calendar 2019の2日目になります。

検索機能?

Webアプリを作成していると、よく検索機能作成しますよね?
↓こんな検索画面です。
2019-12-01_21h39_47.png

このくらいシンプルであれば、ifやEloquentを使って条件を追加するのも悪くないですが、検索項目が多くなってきたり、複雑になってくると、途端に可読性が悪くなったり、めんどくさくなったりすると思います。

その為、Laravelの検索機能の作り方や、検索に特化したPackageなどを、ググったりしたのですが、簡単に検索を実装できるものがなかったので作ってみました。

作ったPackage

https://github.com/fusic/Seaaaaarch/

:sparkles:作るときに気にしたポイント

  • 簡単に検索機能を実装できること(検索実装の為に処理をなるべく書きたくない)
  • どんな状況にも柔軟に検索できること
  • Laravelっぽいこと

:sparkles:Seaaaaarchで出来るようになること

  • Searchableクラスを作成することで、検索条件を書く場所を別クラスに分けれる
  • 検索条件にcallbackを受け付けることで、柔軟に検索できる
  • 簡単な検索は設定を書くだけで検索を実装できる
  • Filterクラスを作成することで、検索条件を共通化できる

基本的な使い方

インストール

composer require fusic/Seaaaaarch

bladeの準備

index.blade.php等に検索用の画面を作成します。
ここは普通のbladeになります。
※名前と住所で検索することを想定

index.blade.php
<form action="{{ route('users.search') }}" method="post">
    @csrf
    <div class='row'>
        <div class='col-md-6'>
            <div class='form-group'>
                <input name="name" />
            </div>
        </div>
        <div class='col-md-6'>
            <div class='form-group'>
                <input name="address" />
            </div>
        </div>
    </div>
    <div style="text-align: center;">
        <button class='btn-default'>検索</button>
    </div>
</form>

Searchableの作成

Seaaaaarchをインストールすると、Searchableが作成できるようになります。
Searchableは検索の実態を記載するクラスになります。

php artisan make:searchable UsersSearch

シンプルな検索であれば、下記のような設定だけで検索を実装することが可能になります。

UsersSearch.php
<?php
namespace App\Search;

use Search\Searchable;

class UserSearch extends Searchable
{
    public function __construct()
    {
        $this->params = [
            // ここに検索条件を記載します
            // 名前 と 住所を検索できるようにします
            'name' => [
                // typeにvalueを設定すると、入力値を完全一致で検索します。
                'type' => 'value'
            ],
            'address' => [
                // typeにvalueを設定すると、入力値を部分一致で検索します。
                'type' => 'like'
            ],
        ];
    }
}

※検索用にvalue、like、in、callback等準備しています。
※Searchableの詳細な設定項目についてはSearchableの設定を参照してください。

コントローラーに検索用アクションを作成

検索用に、上記で作成したformからPOSTを受け取るアクションを作成します。
QueryParser::parse に Searchableクラス のインスタンスを渡すことで、POST値から検索に必要な値を取り出します。
※routesの設定してください。

UsersController.php
public function search() {
    $query = QueryParser::parse(new UsersSearch());
    return redirect()->route('users.index', $query);
}

検索の実行

SeaaaaachをインストールするとEloquentにsearchメソッドを追加します。
searchに対してSearchableインスタンスを渡すことで、 Searchableに設定された検索条件で検索を行います。
searchメソッドの前後にEloquentの機能で条件やソートなどを追加することも可能です。

UsersController.php
public function index() {
    UserModel::search(new UserSearch())->where('example', 'test');
    // or
    UserModel::where('example', 'test')->search(new UserSearch())
}

柔軟な検索

Searchableに検索条件を書くことになりますが、完全一致や部分一致ではなく、JoinやPHPの処理を挟む等複雑な処理をすることもあると思います。
そういう状況では、Searchableのcallbackを利用します。

callbackの引数に渡ってくる$builderでEloquentの機能を利用することが出来ます。

UsersSearch.php
<?php
namespace App\Search;

use Search\Searchable;

class UserSearch extends Searchable
{
    public function __construct()
    {
        $this->params = [
            'name' => [
                // typeにcallbackを指定すると、methodに検索処理を書くことが出来ます。
                'type' => 'callback',
                'method' => function (Builder $builder, $key, $value) {
                    // ここで条件を組み立てます
                    $name = mb_convert_kana($value, 'Hc');
                    $builder->where('name', $name);
                }
            ]
        ];
    }
}

処理の共通化

プロジェクト全体等、共通的に利用したい検索条件を作成する機能もSeaaaaarchは準備しています。
Filterクラスを作成することで共通化することが可能です。

Filterの作成

Filterを作成することで、検索条件を作成することが可能です。

php artisan make:filter ExampleFilter
App\Search\Filter\ExampleFilter.php
<?php
namespace App\Search\Filter;

use Illuminate\Database\Eloquent\Builder;
use Search\Filter\Filter;
use Search\Filter\FilterInterface;

class ExampleFilter extends Filter implements FilterInterface {
    protected $defaultOptions = [];

    public function process(Builder $builder, $field, $value)
    {
        // ここに検索条件の処理を記載します
        // 例:20歳以上を検索条件にする
        $builder->where('age', '>=', 20);
    }
}

Filterの使い方

UsersSearch.php
<?php
namespace App\Search;

use Search\Searchable;

class UserSearch extends Searchable
{
    public function __construct()
    {
        $this->params = [
            'age' => [
                // この指定で、20歳以上の条件がかかるので検索条件の共通化をすることが可能です。
                'type' => ExampleFilter::class
            ]
        ];
    }
}

その他細かい利用方法

GitHubに記載してありますので確認してください。
https://github.com/fusic/Seaaaaarch/tree/master/docs/ja

気に入ったらPull requestやstarよろしくお願いします :bow_tone1:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP の匿名関数に潜む罠

これは 豊田高専コンピュータ部 Advent Calendar 2019 | 3 日目の記事です.

はじめに

つい先日の 11 月 28 日,ついに PHP 7.4 がリリースされました.
プロパティの型ヒンティングが追加されるなど,目玉機能が多いアップデートになりました.
その中の一つに, 匿名関数の新書式 (アロー関数) があります.

PHP 7.3 以前では,匿名関数を以下のように書いていました:

function ($args) use ($globals) {
    return doSomething($args, $globals);
}

PHP の匿名関数 (Closure) は,記述されたスコープを維持しないので, 外は別の世界 となります.
したがって,外のスコープの変数を使用するときは use を用いる必要がありました.

PHP 7.4 からは,以下のように書くことができます:

fn($args) => doSomething($args, $globals)

この 2 つは等価といえますが,後者では use を省略できます.

実は,ここに大きな罠があったのです.
(PHP 7.4 で新しくできた罠ではありませんが,より意識しづらくなってしまいました.)

なにが起きるのか

以下のようなコードを考えてみましょう.

$i = 0;

call_user_func(
    fn() => echo ++$i . PHP_EOL,
);

echo $i . PHP_EOL;

匿名関数の中で $i をインクリメントしたあと出力し,その後グローバルスコープで $i の値を出力するコードです.
一見すると,どちらでも 1 が出力されるように思えてしまいます.

このコードを実行したときの出力は以下のようになります:

1
0

匿名関数内でインクリメントした $i値が維持されていない ことがわかります.

どこが罠なのか

先ほどのコードを PHP 7.3 以前の書式に書き直してみましょう:

$i = 0;

call_user_func(
    function () use ($i) {
        echo ++$i . PHP_EOL;
    },
);

echo $i . PHP_EOL;

この書式にすると use で明示的に外のスコープの $i が使われていると分かります.

しかし,この use における宣言は単なる宣言ではないのです.
匿名関数に入るとき,外のスコープの変数は,仮引数と同じように 値がコピーされます
PHP ではオブジェクト (クラスのインスタンス) や配列も既定で値渡しされます.
したがって,匿名関数内でインクリメントしたのはコピーに過ぎないので, 抜けた後には反映されません

どうすればいいのか

旧書式では,明示的に参照渡しをすることで解決できます:

-    function () use ($i) {
+    function () use (&$i) {

出力は以下のようになるはずです:

1
1

しかし,この方法は 新書式には使えません
なぜなら, use を省略するようになったことで参照渡しの & を明示的に宣言する場所がないからです.
( fn () use (&$i) => ... とすることも今のところできません)

なにが悪いのか

旧書式では use で宣言することで 仮引数の一つのような振る舞い をすることが一目瞭然でした.
しかし,新書式では明示的な宣言がないため, 他の言語 で行えるように外のスコープの変数も読み書きできるように思えてしまいます.

use がないことでその違いに気づけず,罠にはまってしまうのです.

まとめ

近年,セミコロンをはじめ,不要なものはできる限り省略することが増えているのではないかと,個人的に思います.
省略はコードを簡潔に保つうえで大切ですが,可読性を犠牲にしたことでこういった罠が増えることのないようにしていきたいですね. (これは余談ですが,セミコロンは付ける派です.)

釣りのようなタイトルで長々と書いてしまいましたが,この罠にかかって小一時間費やすといった,私のような人が少しでも減ればいいなと思います.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[CakePHP]パスワードリマインダをつくったよ

はじめに

皆さんはパスワードを忘れたことはありますか?
私はあります。

そんな時に便利(?)なのがパスワードを忘れた方はこちらってやつですね。
いわゆるパスワードリマインダとかパスワードリセットとかいうやつです。
今回はCakePHPを使用し、それを作ります。

いろいろなパスワードリマインダの方法

管理者向けパスワードリセット機能

利用者は、パスワードを忘れた場合、管理者に問い合わせて対処してもらいます。
しかし、通常パスワードはハッシュ化されていて管理者さえもパスワードを知ることができないので、
通常はパスワードを再設定する方式が取られます。(今のパスワードがそのまま通知方式が採用されているならやばい)

パスワード再発行の順序として、

  1. 問い合わせを受付、利用者の本人確認
  2. 管理者がパスワードをリセットして、利用者に仮パスワードを伝える
  3. 利用者は仮パスワードでログインして、直ちにパスワードを変更する

となります。

利用者向けパスワードリセット機能

こちらは利用者自らがパスワードをリセットします。
管理者向けパスワードリセット機能はすべてのアプリケーションが備えるべきですが、
利用者向けパスワードリセット機能はセキュリティ強度を下げる原因となるため、サイトの性質により、実装の是非を検討します。

本人確認

通常にアカウントに登録済みのメールアドレスによって本人確認をおこないます。
また、二段階認証を用いることで、本人確認を強化することができます。

しかし、メールは多くの場合平文で送信されることから盗聴のリスクはありますし、二段階認証で生成される
数字は6桁程度ですから、一般に強度不足です。

パスワードの通知

パスワードの通知方法には、以下の4種類が考えられます。
①現在のパスワードとメールで送信する。
 →現在のパスワードを知り得るということは、パスワードはハッシュ化されていないという不安に利用者に与える上、メールが盗聴された際のリスクが最も大きいため、最悪の方法です。
②推測困難なパスワード変更画面のURLで送付する。
 →今回実装する方式です。
③仮パスワードを発行して、メールで送信する。
 パスワードをメールで送るという点で、一見①の方法と変わりないと思われますが、仮パスワードが変更された段階で利用者にメールで通知することによって、不正利用を利用者が気づくことができます。
④パスワード変更画面に直接遷移する。
 →本人確認のメールアドレスが入力後、メールで受信確認のトークンを送付し、アプリケーションでトークンを入力することで、パスワード変更画面へ遷移する方式です。

この項目は、以下の書籍を参考にさせていただきました。
体系的に学ぶ 安全なWebアプリケーションの作り方

パスワードリマインダの実装

使用するテーブル

Users Table

CREATE TABLE Users (
  id int(11) unsigned NOT NULL AUTO_INCREMENT,
  name varchar(20) NOT NULL,
  email varchar(255) NOT NULL UNIQUE,
  password varchar(255)  NOT NULL,
)

ハッシュ化された文字は通常60文字ですが、公式では今後アルゴリズムが変更になる可能性があるので、
255文字が適切だと書いてあります。

PasswordReset.php
<?php
namespace App\Model\Entity;

use Cake\ORM\Entity;
use Cake\Auth\DefaultPasswordHasher;

class PasswordReset extends Entity
    protected function _setPassword($password)
    {
        if (strlen($password) > 0) {
          return (new DefaultPasswordHasher)->hash($password);
        }
    }
}

CakePHPでパスワードをハッシュ化するときには、Entityクラスでセッター機能を使うことで実現できます。
これは、newEntityやpatchEntityなどで、エンティティにセットタイミングでバリデーションの後に発動します。

ちなみに、DefaultPassWordHasherの内部では、passwordHashが呼ばれています。

DefaultPassWordHaser.php
class DefaultPasswordHasher extends AbstractPasswordHasher
{
    protected $_defaultConfig = [
        'hashType' => PASSWORD_DEFAULT,
        'hashOptions' => []
    ];

    public function hash($password)
    {
        return password_hash(
            $password,
            $this->_config['hashType'],
            $this->_config['hashOptions']
        );
    }

password_hashの2つ目の引数は、ハッシュ化に使うアルゴリズムの定数を指定します。
PASSWORD_DEFAULTを指定しておけば、大丈夫です。

PASSWORD_DEFAULT - bcrypt アルゴリズムを使います (PHP 5.5.0 の時点でのデフォルトです)。 新しくてより強力なアルゴリズムが PHP に追加されれば、 この定数もそれにあわせて変わっていきます。 そのため、これを指定したときの結果の長さは、変わる可能性があります。 したがって、結果をデータベースに格納するときにはカラム幅を 60 文字以上にできるようなカラムを使うことをお勧めします (255 文字くらいが適切でしょう)。
https://www.php.net/manual/ja/function.password-hash.php

3つ目の引数は、指定すると手動でソルトを設定することができます。が、非推奨です。
省略するとパスワードをハッシュするたびにランダムなソルトを自動生成します。

UsersTable.php
public function validationDefault(Validator $validator)
{
    $validator
            ->notEmpty('password', 'パスワードが入力されていません。')
            ->minLength('password', 8,  'パスワードは8文字以上で入力してください。')
            ->alphaNumeric('password', 'パスワードには半角英数字のみ使用できます。')
            ->add('password', 'numberAndAlpha',[
                'rule' => function($data, $context) {
                        $valid = preg_match('/\A(?=.*?[a-z])(?=.*?\d)[a-z\d]/i', $data);
                        return $valid ? true : 'パスワードは英文字、数字それぞれ1文字以上含める必要があります。';
                    }
            ])
            ->sameAs('password', 'password_check', '確認用のパスワードと一致しません。');      
}

バリデーションは、tableクラスで定義します。
エンティティのバリデーションは、patchEntity、newEntity,またはsaveが呼ばれた時に実行されます。
validationの詳しい解説はまた今度にも。

PasswordResets Table

CREATE TABLE PasswordReset (
  id int(11) unsigned NOT NULL AUTO_INCREMENT,
  email varchar(255) NOT NULL,
  selector varchar(255)  NOT NULL,
  token varchar(255) NOT NULL,
  expire datetime NOT NULL
)

IDいらなくね?ってなるけどとりあえず規約には従っておきましょう。

PasswordReset.php
<?php
namespace App\Model\Entity;

use Cake\ORM\Entity;
use Cake\Auth\DefaultPasswordHasher;

class PasswordReset extends Entity
    protected function _setToken($password)
    {
        if (strlen($password) > 0) {
          return (new DefaultPasswordHasher)->hash($password);
        }
    }
}
PasswordResetsTable.php
変更点なし

パスワードを忘れた方へのページ

View

はじめにこれがないと始まらないですね。
viewはざっくりと会員に紐付いているメールを送らせるFormを作ります。

forget.ctp
<?php
echo $this->Form->create();
echo $this->Form->control('email', ['type' => 'email']);
echo $this->Form->end();

Controller

UserControllerのforgerメソッドで処理をします。

UserController.php
<?php

namespace App\Controller;

use App\Controller\AppController;
use Cake\Mailer\Email;
use Cake\ORM\TableRegistry;
use Cake\Routing\Router;
use Cake\Network\Exception\NotFoundException;

class UserController extends AppController
{
    public function forget()
    {
        if ($this->request->is('post')) {
            $email = $this->request->getData('email');
            $users_table = TableRegistry::get('Users');
            $users_table = $users_table->find()
                ->where(['email ' => $email])
                ->first();

            if ($user) {
                $password_resets_table = TableRegistry::get('PassWordResets');
                $password_reset = $password_resets_table->find()
                    ->where(['email' => $email])
                    ->first();
                if ($password_reset){
                    // expireを更新するために、すでにテーブルに登録されていたら削除する
                    $password_resets_table->delete($password_reset);
                }
            } else {
                // 未登録のメールアドレスの場合は、なにもせずに結果画面を表示
                return $this->redirect('msg');
            }

            $data['email'] = $email;
            $data['selector'] = bin2hex(random_bytes(8));
            $data['token'] = random_bytes(32);
            $data['expire'] = date("Y-m-d H:i:s",strtotime("1 day"));
            $url = Router::url([
                'controller' => 'User',
                'action' => 'reset',
                '?' => ['selector' => $data['selector'], 'token' =>bin2hex($data['token'])],
            ], true);

            $password_reset = $password_resets_table->newEntity($data);
            $password_resets_table->save($password_reset);

            $email = new Email('default');
            $email->from(['me@example.com' => 'My Site'])
              ->to($email)
              ->subject('パスワード再発行のお知らせ')
            $email->emailFormat('text');                            
            $email->template('templete');                     
            $email->viewVars($url);
            return $this->redirect('msg');
        } 
    }
    public function msg()
    {
    }
}

postデータを受け取ったら、実際に会員データに登録されているメールアドレスか調べます。

$email = $this->request->getData('email');
$users_table = TableRegistry::get('Users');
$users_table = $users_table->find()
   ->where(['email ' => $email])
  ->first();

会員のメールアドレスではなかった場合、当然メールは送信しませんが、
必ずメール送信に成功した時と同じ画面を表示しましょう。

} else {
    // 未登録のメールアドレスの場合は、なにもせずに結果画面を表示
    return $this->redirect('msg');
}

これは、存在しないメールアドレスですなどのエラーメッセージを出してしまうと、
第三者にメールアドレスが登録していないことがわかってしまうからですね。

存在するメールアドレスなら、処理を続行します。
まず、すでにPassWordReset Tableに登録しているユーザーであるなら、これを削除します。
基本的に、パスワードの変更が完了した場合には、このテーブルの行は削除されますが、
新たにパスワード再発行依頼をしたときにこれを削除しないと、有効期限が上書きされません。

if ($user) {
    $password_resets_table = TableRegistry::get('PassWordResets');
    $password_reset = $password_resets_table->find()
        ->where(['email' => $email])
        ->first();
    if ($password_reset){
        // expireを更新するために、すでにテーブルに登録されていたら削除する
        $password_resets_table->delete($password_reset);
    }

次に、トークンと有効期限を発行して、URLを生成します。
トークンにはselectorとtokenの2種類ありますね。これらはどう使われるのでしょうか。

$data['email'] = $email;
$data['selector'] = bin2hex(random_bytes(8));
$data['token'] = random_bytes(32);
$data['expire'] = date("Y-m-d H:i:s",strtotime("1 day"));
$url = Router::url([
       'controller' => 'User',
       'action' => 'reset',
       '?' => ['selector' => $data['selector'], 'token' =>bin2hex($data['token'])],
       ], true);

$password_reset = $password_resets_table->newEntity($data);
$password_resets_table->save($password_reset);

URLを得るのは簡単です。Router::urlを使用します。
これは、controllerとactionを指定すると、自動でルーティングに基づいたURLを生成してくれます。
今回は、getパラメーターも指定したいので、'?'をキーに配列に加えましょう。
第2引数をtrueにすると、フルパスで生成することができます。。

tokenは、さきほどEntityクラスで見たように、DefaultPasswordHasherによってハッシュ化されます。
もしも、トークンの情報を盗み見られてしまった場合、トークンがそのまま保存されていたら誰でもそのURLにアクセスできてしまうので、
パスワードを乗っ取られてしまいます。

そのためトークンもパスワードと同じように、ハッシュ化して保存してあげる必要があるわけですね。
ハッシュ化した場合、そのトークンで直接データベースからデータをもってこれないので、
データベースから検索するようにselectorというトークンを使用するわけです。

あとは生成したURLを添付したメールを送ってあげましょう。
CakePHPのEメールに関しては公式の情報を参照してください。
https://book.cakephp.org/3/ja/core-libraries/email.html

メールを受け取ったユーザーがパスワード再設定ページへアクセス

ユーザーはこんな感じのメールを受け取るので、メール本文のURLへアクセスします。

パスワード再発行のリクエストを受け付けました。
下記リンクをクリックしていただき、パスワード再設定の登録をお願いいたします。
*リンクの有効期限は、24時間です。
ttps://example.com/reset/?selector=4b148a081b9be8f9&token=e63f46d098187d3c0298cca07450963e32dd90d9d70e3f414e3d9f120be567a6
本メールにもしお心当たりのない場合、
恐れ入りますが破棄して頂けるようお願いいたします。

View

よくある、パスワードと確認用のパスワードを入力させるやつです。

reset.ctp
<?php
echo $this->Form->create(['url' => ['action' => 'reset_ok']]);
echo $this->Form->control('password');
echo $this->Form->password('password_check');
echo $this->Form->end();

ここはgetメソッドだけで入ってこられるようにしたいので、post先は他の場所を指定します。

Form->controlを使うと、カラム名がpasswordなら、勝手にパスワードフォームになってくれます。
確認用のパスワード入力フォームのnameには、UsersTableのバリデーションのSameAsの第2引数で渡した名前にします。
これですね。

->sameAs('password', 'password_check', '確認用のパスワードと一致しません。');  

バリデーションは、テーブルにないカラム名でも、同じrequestに入っていた時に一緒に来てくれます。

Controller

UsersController.php
function reset() {
    if(!$this->request->isget()){
        throw new NotFoundException();
    }

    if (!$this->request->query('selector') || !$this->request->query('token')) {
        throw new NotFoundException();
    }

    $password_resets_table = TableRegistry::get('PassWordResets');
    $password_reset = $password_resets_table->find()
        ->where([
            'selector' => $this->request->query('selector'),
            'expire >=' => date("Y-m-d H:i:s"),
         ])
        ->first();

    if (!$password_reset) {
        throw new NotFoundException();
    }

    if (!password_verify(hex2bin($this->request->query('token')), $password_reset->token)){
        throw new NotFoundException();
    }
    $this->session->write('email', $password_reset->email);
}

まず、getメソッド以外はすべて突き返します。
NotFoundExceptionはその名の通り、404エラーを発生させます。

selectorまたはtokenが取得できなかったときも同様です。

requestの取得に成功した場合、トークンをもとにデータベースからどのユーザーのリクエストなのかを特定します。

$password_resets_table = TableRegistry::get('PassWordResets');
$password_reset = $password_resets_table->find()
    ->where([
        'selector' => $this->request->query('selector'),
        'expire >=' => date("Y-m-d H:i:s"),
     ])
     ->first();

tokenはハッシュ化しているため、こいつをもとにデータを持ってくることはできません。
(password_hashは、毎回自動生成されたソルトをもとにハッシュ化しているので、同じ文字列をハッシュ化しても同じ二度と同じハッシュは得ることはできません)
そのために、selectorも用意したのでしたよね。

同時に有効期限が切れていないかどうかも調べます。

データを取得できなかったときもNotFoundを投げます。
(ところで有効期限切れのURL404でよかったんでしたっけ?よくわからんです。)

tokenが正しいかどうかは、password_verifyを使って調べます。

if (!$password_reset) {
    throw new NotFoundException();
}

if (!password_verify(hex2bin($this->request->query('token')), $password_reset->token)){
    throw new NotFoundException();
}
$this->session->write('email', $password_reset->email);

getから送られてくるtokenは16進数化しているので、hex2binでバイナリ文字列にデコードする必要があることに注意しましょう。
password_verifyは第1引数にパスワードを、第2引数にハッシュ値をしていして、一致するとtrueを返します。

っていうかpassword_verifyってソルトとかどうやって計算してるの?なんかよくわからんけどすごい。

tokenも一致したらOK。セッションでemailを保持します。

再設定のパスワード受け取り、登録

UsersController.php
function reset_ok() {
    if ($this->request->ispost()) {
        $email = $this->seeeion->read('email');
        $this->session->delete();
        if (!$email){
           throw new NotFoundException();
        }

        $users_table = TableRegistry::get('Users');
        $user = $users_table->find()
            ->where(['email' => $email])
            ->first();

        $users_table->patchEntity($user, $this->request->getData());
        if ($user->errors()) {
            return $this->redirect($this->referer());
        }
        $password_resets_table = TableRegistry::get('PassWordResets');
        $password_reset = $password_resets_table->find()
            ->where(['email' => $email])
            ->first();
        $password_resets_table->delete($password_reset);

        if ($users_table->save($user)) {
            $this->Flash->success(__('パスワードを変更しました。'));
            return $this->redirect(['action' => 'login']);
        } else {
            $this->Flash->error(__('パスワードの変更に失敗しました。');
            return $this->redirect(['action' => 'login']);
        }
    } else {
        throw new NotFoundException();
    }
}

postからパスワードを受け取って、セッションからemailを取得。
セッション切れなどでemailが取得できなかったら返してやりましょう。

そしたらemailからパスワードを変更するユーザーを取得します。

パスワードのバリデーションですが、これはUserstableで定義済みなので、pacthEntityしたら全部やってくれます。
で、errorsメソッドを使ってバリデーションエラーが発生したか判定すればOK。

// controllerのバリデーションはこれだけでOK!
$users_table->patchEntity($user, $this->request->getData());
if ($user->errors()) {
   return $this->redirect($this->referer());
}

エラーメッセージもForm->controlを使用していれば勝手に出してくれます。超便利。

バリデーションも通ったら、PasswordResetテーブルからもトークンの情報を消し去りましょう。
これで一度パスワードを変更したら同じURLではアクセスできなくなります。

$password_resets_table = TableRegistry::get('PassWordResets');
$password_reset = $password_resets_table->find()
    ->where(['email' => $email])
    ->first();
$password_resets_table->delete($password_reset);

あとはtableのsaveメソッドでパスワードを変更したら完了です。
loginページに飛ばしてやればいいんじゃないでしょうか。
これで終わりです。お疲れさまでした。

最後に

こちらのサイト(Youtube)を参考にさせていただきました。
How To Create A Forgotten Password System In PHP | Password Recovery By Email In PHP | PHP Tutorial

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP基礎 Part1

概要

PHPの独学で学んだ内容をアウトプットしていく

PHPとは

HTMLを動的に表示させ、サーバー側で処理を実行することに適した言語(見る人や時間に応じてテキストが変化する)
HTMLで記述したWebページは静的なコンテンツとなる = 逐一運用しない限り、いつどこで誰が見ても同じ内容となる

PHPの用途

主にWeb系サービスの開発で使用されるプログラミング言語
ブログ、SNS、ECサイト、お問合せフォーム、各種サービスのログイン画面..etc

PHPの書き方

HTMLにPHPのコードを埋め込んで書くことができる。
他にも外部ファイルを呼び出す形式があるとのことだが、今回は省略。
尚、ファイル名は「〇〇.php」とする

PHPの基礎文法と文字列の出力

PHPの文法の最低限満たす必要のあるルールは以下の2つ

1.開始タグと終了タグ
PHPは開始タグ「<?php」から始まり、終了タグ「?>」」で終える。

2.コードの末尾にセミコロン「;」を記述
コードの末尾にセミコロン「;」を書かないとエラーになる
※可読性を考慮してタブ、半角スペースを入れると尚良

これらを踏まえたものが以下のコードとなる

index.php
<?php
  echo "Hello World"; //出力結果はHello World
?>

echoは文字列を出力する際の命令文である
文字列を出力する場合は、ダブルクォーテーション「" "」かシングルクォーテーション「' '」で文字列を挟むこと

計算結果の出力

簡単な計算結果(主に四則演算)についても、以下のように出力可能

index.php
<?php
  echo 2 + 3; //出力結果は5
  echo 5 - 2; //出力結果は3
  echo 2 * 5; //出力結果は10
  echo 6 / 3; //出力結果は2
  echo 7 % 3; //出力結果は1(割り算の余り = 商)
?>

但し、計算式を文字列で囲うと計算式そのものが文字列として出力される

index.php
<?php
  $money = 100;
  echo "2 + 3"; //出力結果は"2 + 3"
?>

次回

  • データ型
  • 変数
  • その他
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

2019年phpカンファレンス① ~MVCとはなにか~

2019年phpカンファレンスでの『MVCとはなにか』の講演について、思ったことをまとめてみました。

講演資料

https://speakerdeck.com/tenjuu99/what-mvc-is?slide=2

この講演を聞いたきっかけ

この前プライベートでLaravelを触ってみました。
会社のシステムは違うフレームワークでやっているため、結構新鮮だったとともに「model」の考え方が全然違った。

今までmodelは「いろんなところで使っている関数をまとめる場所」というイメージでした。
そのため、DB接続系の処理も書いていたが、Laravelでは書かない。みたいな記述を見つけて、「modelって何書けばいいんだろう、、、」と思っていました。

そこで、modelの概念について勉強しようと思い、この講演を聞いてみました。

講演を聞いて

「modelってなに、、、?」

という疑問がより深まりました(笑)

なぜそうなったかは、これからゆっくり書いていきます。

内容(解釈をかなり入れております)

システム作ってよ!

大体のシステム開発はここから始まるかと思います。
「こういう不便なところがある」というニーズから、それを解決するために開発を行っていくのが通常です。
その問題を解決するためにエンジニアは、「処理で問題解決する」という手法と「動きで問題解決する」という二つの方法を取るのが一般的です。

それらを解決するために実装する。それが実装されるのが、「controller」や「view」なのかなぁと思います。

受け取ったデータに関して、こういう風に処理して、こういうデータを格納する。これがcontrollerの役割。

ここを押したら、こうなるだろうなぁというユーザビリディを考え、思った通りに動くシステムにする。これがviewの役割。

じゃあ、modelの役割は、、、?

個人ごとの問題

同じシステムを使っていても、持っている課題は別のものになることは多いです。
ユーザAはαという点に課題を感じていて、ユーザBはβという課題を抱える。しかもそれをそれぞれ解決しないといけない。
このような場面では、controllerやviewを使い、個別で解決してあげる必要があります。
うまく表面をチューニングしてあげることができるのもシステム運用では大事なため、その役割を持っています。

じゃあ、modelの役割は、、、?

問題の根本を見抜く

αという課題でも、βという課題でも、その根本にある課題は同じであることは多いです。
それを解決するのが、modelの役割かと思います。

今まではテーブルⅠしか更新してなかったけど、ユーザの要望でテーブルⅡも一緒に更新するようにする。

こういうシステムの根本に関わることを解決するのが、modelの役割になります。

自分が考えたmodelのあり方とは

modelとは、「システム思想の反映」だと思いました。

このシステムはなぜ作られたのか、何のためにつくられたのか。が反映しているものにすべきではないかと思いました。

modelを見るとそのシステムがなぜ作られたのか、どういう風になっていくべきなのか。が伝わるのではないでしょうか?

ユーザの問題をちゃんとキャッチアップして、その根本を見ていく、そしてそれを反映させていく
という箇所がmodelの役割かなぁと感じました。

modelってなに、、、?

はじめに何度も書いたのですが、改めて自分の関わっているシステムを考えた時、そのシステムのmodelのあり方って難しいなぁと思いました。

システムが社会にどういう影響を与えていて、どういう風に発展していくべきか。そういうことを考えながら、組んでいく必要がある部分で、システムそのものが表現されている部分だからこそ、もっともっと深く考えるべきだなぁと思いました。

最後に

https://speakerdeck.com/tenjuu99/what-mvc-is?slide=2

そう思って再度資料を見ていただければ、何となーく感じ方が変わるのかなぁ、、、
今回の講演で言いたかったことを10%も吸収できてない気がするけど、とりあえず思ったことを書いてみました。
これ違うよってことがあれば是非コメントください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP カンファレンス 2019 登壇資料まとめ

PHP カンファレンス 2019

https://phpcon.php.gr.jp/2019/

タイムテーブル

https://fortee.jp/phpcon-2019/timetable

登壇資料

敬称略

Track 1

PHPの今とこれから2019
廣川 類


コミュニティアップデート powered by GMOインターネット
GMOインターネット株式会社


思想と理想の果てに -- クリーンアーキテクチャのWEBフレームワークを作ろう
成瀬 允宣
https://nrslib.com/phpcon-2019-proposal/


PHPUnit: Past, Present and Future
Sebastian Bergmann


オニギリペイのセキュリティ事故に学ぶ安全なサービスの構築法
徳丸浩
https://www.slideshare.net/ockeghem/phpconf2019

Track 1 - LT

Laravel + Nuxt.js + FirebaseでDXと開発コストを意識したSPA開発
カンボ@沖縄
https://speakerdeck.com/bumptakayuki/laravel-plus-nuxt-dot-js-plus-firebasetedxtokai-fa-kosutowoyi-shi-sitaspakai-fa


SwooleとWebpで遅いページを高速化する
寺井 諒


PHPでgRPCってどこまでいけるの?
株式会社サイバーエージェント 白井 英
https://speakerdeck.com/sgeengineer/phpdegrpctute-dokomadeikerufalse


PHP-CS-FixerをIDEに取り込ませてPSRを強制する開発スタイル
sogaoh
https://gitpitch.com/sogaoh/myslide?p=20191201-phpcon2019-LT#/


レガシーコードでビジュアルリグレッションテストをやってみた
阿波連 智恵
https://speakerdeck.com/aharenchi/regasikodode-biziyuaruriguretusiyontesuto-woyatutemita


PHP on AWS Lambda!
清家史郎
https://slide.seike460.com/slides/phpcon2019#/


2年目エンジニアがスキルアップのためにPHPで競プロやってみた


設計文化のないチームに文化を広めたが冴えない一手で混沌を招いた話を聞いてほしい
しろぐちゆうま
https://speakerdeck.com/mashirou1234/she-ji-wen-hua-falsenaitimuniwen-hua-woguang-metagahu-enai-shou-dehun-dun-wozhao-itahua-wowen-itehosii


運用経験ばかりのメンバーと新規開発で初めてしっかりと設計から開発までを経験した話
大重智志
https://speakerdeck.com/ohshige/php-conference-japan-2019-lt


エンジニアがブログや登壇で、アウトプットするとどうなる?
うゐろう
https://www.nyamucoro.com/entry/2019/09/21/004626


余裕を生み出すコードレビュー
高野福晃
https://speakerdeck.com/fortkle/code-review-phpcon-2019


社内最長老のシステムにPHPUnitで立ち向かう方法
やなせ たかし
https://speakerdeck.com/penguin045/she-nei-zui-chang-lao-falsesisutemuniphpunitdeli-tixiang-kaufang-fa


開発合宿のススメ!
Hamee株式会社 田島 裕介

Track 2

PHP における並列処理と非同期処理入門
めもり〜
https://speakerdeck.com/memory1994/php-niokerubing-lie-chu-li-tofei-tong-qi-chu-li-ru-men


徳丸先生による徳丸試験例題解説とPHP7初級書籍贈呈キャンペーンと市場動向
吉政忠志、ゲスト解説:徳丸浩先生


PHP 開発環境で使う Kubernetes
新原 雅司
https://speakerdeck.com/shin1x1/php-development-environment-on-kubernetes


20年前のMySQL、今のMySQL
yoku0825


Webサービスのトラブルの現場 ~ Webサービスの今と昔 ~
soudai
https://speakerdeck.com/soudai/web-server-is-dead


REST 6+4の制約
郡山昭仁
https://speakerdeck.com/koriym/rest-6-plus-4falsezhi-yue

Track 3

Chatworkのシステムから学ぶレガシーなPHPの限界とレガシーからの脱却
村上 俊介
https://speakerdeck.com/shmurakami/phpcon2019


PHPからgoへの移行で分かったこと
樋口雅拓
https://speakerdeck.com/mahiguch/phpkaragohefalseyi-xing-defen-katutakoto


「弁護士ドットコム」を作り続ける開発組織について
狩野 秀明
https://speakerdeck.com/bengo4com/about-bengo4com-development-organization


改善失敗から学ぶ、レガシープロダクトに立ち向かうチーム作り。
杉山 祐一
https://speakerdeck.com/oogfranz/gai-shan-shi-bai-sitexue-bu-regasipurodakutonili-tixiang-kautimuzuo-ri


プログラム未経験からたった3ヶ月で圧倒的な開発力を身につける ~ スクラッチ開発の重要性
前田直哉


脆弱性から学ぶWebセキュリティ
バーチー
https://speakerdeck.com/hypermkt/study-web-security-from-vulnerability2


このPHP QAツールがすごい!2019
sasezaki
https://www.slideshare.net/sasezaki/php-qa2019

Track 4

MVCにおける「モデル」とはなにか
天重誠二
https://speakerdeck.com/tenjuu99/what-mvc-is


『グランブルーファンタジー』開発エンジニアの考え方-PHP7が『グラブル』にもたらしたブレイクスルーと考察-
小松 美穂


新しい概念のWAFが叶える、クラウドネイティブ時代のセキュリティ
EGセキュアソリューションズ株式会社 代表取締役 徳丸 浩(株式会社グレスアベイル 社外取締役)・株式会社グレスアベイル 取締役 CTO 根岸 寛徳


知見のない技術スタックをプロダクション導入するエンジニアの導入戦略
東口和暉
https://speakerdeck.com/hgsgtk/a-strategy-to-choice-no-knowledge-technology


Laravel × クリーンアーキテクチャ 開発中の現状をお伝えしたい
ブライソン イアン
https://speakerdeck.com/ianbrison/laravel-x-clean-architecture-xin-gui-kai-fa-zhong-falsexian-chang


15年続くWebサービスへのCI/CDとテストコードの導入
9bo9bo
https://speakerdeck.com/9bo9bo/cdtotesutokodofalsedao-ru

Track 5

PHP初心者セッション
柏岡秀男


PHPerのためのテストコード入門
02 / 大津 和槻
https://speakerdeck.com/cocoeyes02/phperfalsetamefalse-tesutokotoru-men


Composerって何?どう動くの?読んでみました!
きんじょうひでき
https://speakerdeck.com/o0h/lets-read-composer


PHPは何を捨て、どんな力を手に入れてきたのか
うさみけんた


Zend VMにおける例外の実装
hnw
https://www.slideshare.net/hnw/zend-vm


PHPを学ぶということ
岸田健一郎
https://speakerdeck.com/sizuhiko/phpcon-2019


Apache から LiteSpeed に乗り換えてみませんか?
高橋邦彦

Track 6

Hash, Cryptography, and PHP
Peter
https://slides.com/peter279k/php-con-japan-2019/#/


Putting legacy to REST with middleware
Adam Culp
https://www.slideshare.net/adamculp/putting-legacy-to-rest-with-middleware


5ヶ月でカバレッジを20%から90%にした話
吉田あひる
https://speakerdeck.com/yahiru/5keyue-tekaharetusiwo20-percent-kara90-percent-niaketahua


「CPUとは何か」をPHPで考える
長谷川智希
https://speakerdeck.com/tomzoh/what-is-a-cpu


How to Supercharge your PHP Web API
Aurimas Niekis

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker+Laravel+OpenAPIGenerator

はじめに

OpenAPIGeneratorを使って生成されたLaravelのソースコードで環境構築してみました。
DockerでLaravel環境の構築~スタブサーバの生成、リクエストの確認まで行います。

プロジェクトディレクトリ構成

project/
 ├ www/                 # Laravel Project Container
 ├ generator/           # generator Container
 ├ docker-compose.yml
 ├ oas.yml

Sample OAS

プロジェクト直下にoas.ymlを用意します。
簡易的なCRUDが行えるAPIを想定しています。

oas.yml
openapi: 3.0.0
info:
  title: Task API
  version: 0.0.1
servers:
  - url: http://localhost
    description: Laravel Server
tags:
  - name: Task
paths:
  /api/task:
    get:
      tags: [ "Task" ]
      summary: Show Task List.
      description: Show Task List.
      operationId: listTask
      responses:
        '200':
          description: Successful response
    post:
      tags: [ "Task" ]
      summary: Create Task One 
      operationId: createTask
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                title:
                  type: string
                sort:
                  type: integer
      responses:
        '200':
          description: Successful response
  /api/task/{tid}:
    get:
      tags: [ "Task" ]
      summary: Show Task One.
      description: Show Task One.
      operationId: showTask
      parameters:
        - name: tid
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Successful response
    put:
      tags: [ "Task" ]
      summary: Update Task One.
      operationId: updateTask
      description: Update Task One.
      parameters:
        - name: tid
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                title:
                  type: string
                sort:
                  type: integer
      responses:
        '200':
          description: Successful response
    delete:
      tags: [ "Task" ]
      summary: Delete Task One.
      operationId: deleteTask
      description: Delete Task One.
      parameters:
        - name: tid
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Successful response

Docker環境を用意する

docker-compose.ymlをproject直下に用意します。

docker-compose.yml
version: '3.4'
services:
  generator:
    build: ./generator
    volumes:
      - .:/app
    tty: true
    command: sh

  www:
    build: ./www
    volumes:
    - ./www:/var/www
    ports:
      - 80:80

generatorコンテナはOASからPHPコードを生成するコンテナになります。
wwwコンテナはApacheで動作するLaravel環境のコンテナになります。

続いて、generatorコンテナを構築するためのDockerfileをgenerator/配下に用意します。

FROM openjdk:8-jdk-alpine

WORKDIR /app

RUN apk --update add bash maven git
RUN rm -rf /var/cache/apk/*ls
RUN git clone https://github.com/openapitools/openapi-generator /generator
RUN cd /generator && mvn clean package

OpenAPIGeneratorはJVMでありJava環境が必要なためopenjdk:8-jdk-alpineをベースにしています。
ライブラリのインストーラとしてmavenを使用します。

続いて、wwwコンテナを構築するためのDockerfileをwww/配下に用意します。

FROM php:7.3-apache

RUN apt-get update && apt-get install -y git libzip-dev libxml2-dev
RUN docker-php-ext-configure zip --with-libzip
RUN docker-php-ext-install pdo_mysql mbstring zip xml
RUN curl -sS https://getcomposer.org/installer | php
RUN mv composer.phar /usr/local/bin/composer
RUN a2enmod rewrite && a2enmod headers
RUN sed -ri -e 's!/var/www/html!/var/www/public!g' /etc/apache2/sites-available/*.conf
RUN sed -ri -e 's!/var/www/!/var/www/public!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf
RUN usermod -u 1000 www-data && groupmod -g 1000 www-data
​
ENV COMPOSER_ALLOW_SUPERUSER 1
ENV COMPOSER_HOME /composer
ENV PATH $PATH:/composer/vendor/bin

WORKDIR /var/www
​
RUN composer global require "laravel/installer"

今回はApache上で動作するPHP(php:7.3-apache)をベースにしています。
Laravelを使いたいのでcomposerをインストールしています。
URLを書き換えやリダイレクトを有効にするため、a2enmod rewrite && a2enmod headersを実行しています。
sedでLaravel用のドキュメントルートに変更しています。
Apacheを動かすwww-dataユーザがLaravelから吐き出されるlogファイルにアクセスできるように権限を付与しています。

上記の準備ができたらコンテナを起動します。
各リソースのDLやInstall処理が一気に走るため結構な時間を要するかと思います。

$ docker-compose up -d --build

Laravel用のソースコードを自動生成する

OpenAPIGeneratorを使用してソースコードを生成します。
generatorコンテナに入って作業します。

$ docker-compose exec generator bash
bash-4.4# java -jar /generator/modules/openapi-generator-cli/target/openapi-generator-cli.jar generate \
   -i oas.yml \
   -g php-laravel \
   -o server-generate
bash-4.4# cp -rf server-generate/lib/. www

上記を実行すると、server-generateに自動生成されたPHPのソースコードが出来上がると思います。
生成されたソースコード一式はwwwコンテナへコピーします。
ちなみに、自動生成できるコードの種類はこちらから確認できます。

生成されたソースコードからLaravel環境を立ち上げる

$ docker-compose exec www bash
root@5295f267d6a1:/var/www# composer install
root@5295f267d6a1:/var/www# cp .env.example .env
root@5295f267d6a1:/var/www# php artisan key:generate

上記の実行が済めば、http://localhost にアクセスして下記の画面を確認することができると思います。
1.png

続いて、生成されたコードが機能しているのか確認してみます。
php artisan route:listを実行してAPIのエンドポイントを確認してみます。

$ docker-compose exec www bash
root@5295f267d6a1:/var/www# php artisan route:list
+--------+----------+----------------+------+------------------------------------------------+------------+
| Domain | Method   | URI            | Name | Action                                         | Middleware |
+--------+----------+----------------+------+------------------------------------------------+------------+
|        | GET|HEAD | /              |      | App\Http\Controllers\Controller@welcome        | web        |
|        | POST     | api/task       |      | App\Http\Controllers\TaskController@createTask | api        |
|        | GET|HEAD | api/task       |      | App\Http\Controllers\TaskController@listTask   | api        |
|        | DELETE   | api/task/{tid} |      | App\Http\Controllers\TaskController@deleteTask | api        |
|        | GET|HEAD | api/task/{tid} |      | App\Http\Controllers\TaskController@showTask   | api        |
|        | PUT      | api/task/{tid} |      | App\Http\Controllers\TaskController@updateTask | api        |
+--------+----------+----------------+------+------------------------------------------------+------------+

エンドポイントの確認が行えたので、続いてswagger-editorからGETリクエストを試したいと思います。
ただ、現時点のままだとCORSに引っかかりリクエストできません。
そのため、リクエストのテストを行う前にCORS対策を行います。
まずはbarryvdh/laravel-corsをインストールします。

$ docker-compose exec www bash
root@5295f267d6a1:/var/www# composer require barryvdh/laravel-cors

www\config\app.phpに下記を追加します。

'providers' => [
    // ...
    Barryvdh\Cors\ServiceProvider::class,
]

app/Http/Kernel.phpに下記を追加します。

'api' => [
    // ...
    \Barryvdh\Cors\HandleCors::class,
],

設定ファイルを以下のコマンドで作成します。

root@02eec0e47db9:/var/www# php artisan vendor:publish --provider="Barryvdh\Cors\ServiceProvider"

設定が完了したので、swagger-editorでリクエストテストを行ってみます。
222.png
無事にGETリクエスト出来たことを確認しました。

以上、Laravel用のスタブサーバの生成および構築までの流れでした。
自動生成されたコードはスタブなため、実処理は開発者が実装していくことになります。
何かの参考になれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPカンファレンス2019 当日レポ

公式サイト

https://fortee.jp/phpcon-2019
今年はBeyondがテーマになっています。

PHPの今とこれから2019

資料: https://www.slideshare.net/hirokawa/presentations

  • 歴代のスライド資料あげました
  • PHP7.4の次は8
    • 2021年の後半(9月?)にリリースかも
  • PHP7.xは80%程度利用中(会場内アンケート)
    • データではPHP7.xは4割程度
    • PHP5.xは57%(昨年は75%)
  • 今年出たバージョンは3年後にはEOL
    • 7.1のEOLは今日(2019/12/01)
  • CVEが付いている修正はセキュリティの修正なので速やかにバージョンアップしたほうがいい
  • PHP7.4は7.3より15%程度ベンチマークが改善
    • ※体感はあまり変わらないが
  • PHP7.4
    • TypeHintingの追加
    • Null許容型
    • OpCacheのPreロードで高速化
    • アロー関数
    • ??=構文
    • mb_str_split関数
    • .演算子の優先順位 +のほうが優先される
    • 弱い参照
    • 継承した型に対してWarningが出ないように
  • PHP8
    • JITエンジンを活用 →より速くなる

Chatworkのシステムから学ぶレガシーなPHPの限界とレガシーからの脱却

資料: https://speakerdeck.com/shmurakami/phpcon2019

  • レガシーコードの定義
    • 修正/拡張/作業が非常に難しいコード
  • PHPとScalaを使ってる
    • PHPはレガシー 巨大なモノリス
    • Scalaは分散システム志向 HBase/DynamoDB/Kafka
    • 3年半前にPHPからScalaへのゼロベース移行
  • PHPの辛み
    • オレオレフレームワーク ※遅くはないし悪くもない
    • 静的解析ツールはPhpMetricsが動かせた
  • なぜこうなった?
    • スピードを優先した結果
    • ビジネスが拡大したタイミングで仕様を厳格化しなかった
    • 全体的なScala化に失敗し、部分移行になった
  • 何が問題なのか
    • データベースの分割 クライアント1万問題
    • データベースのスケールに限界がある
  • マイクロサービスは辛い
    • まずはモジュール分割から
    • チーム間のコミュニケーションも辛い
    • ホラクラシー組織を検討中!
  • レガシーなPHPの限界は、関連ツールの限界がきたとき
  • ビジネスのスケールに合わせて適切なタイミングでリファクタリングしよう

PHPからgoへの移行で分かったこと

資料: https://speakerdeck.com/mahiguch/phpkaragohefalseyi-xing-defen-katutakoto

  • 移行前は ALB->EC2(fpm)->DBなど
    • FuelPHPを使用(メンテナンスが少ない)
  • 移行後は NLB経由でコンテナでGo
  • PHPとGoのmemcacheのライブラリが異なるためHashが合わない
    • memcacheへアクセスするだけのGatewayコンテナを作成
  • PHPerしかいなかったけど10時間くらいでGo書けるレベルになった
  • 記事推薦システムをPythonからGoへ移行
    • 記事とユーザーをベクトル化してパーソナライズする
  • PHPはプロセスを分けて並列化したが、Goは簡単にできる
  • PHPは飽きるのでGoを書くとモチベーションが上がる!
  • GopherはPHPの1/100くらいの感覚
  • PHP/Pythonに戻すかも^^
    • Chatworkさんみたいに5年も頑張れない、、。

思想と理想の果てに -- クリーンアーキテクチャのWEBフレームワークを作ろう

資料: https://nrslib.com/phpcon-2019-proposal/

  • クリーンアーキテクチャはフレームワーク非依存
  • この仕様をどこに実装したっけ?をなくそう
  • ポートアンドアダプター
    • ヘキサゴナルアーキテクチャ
    • 空いているポートにアダプターを繋ぐイメージ
  • DataAccessModuleに主導権を持たせず、BusinessLogicに持たせる
    • クリーンアーキテクチャは内側が外側のことを知らない
  • Entity
    • ビジネスロジックをドメイン化したモジュール
  • アプリケーションレイヤー
    • ユースケースを提供する
  • InterfaceAdapters
    • コントローラ(コントローラ)、画面(プレゼンター)、ゲーム機(インタラクター)
  • Framework&Devices
    • ビジネスロジックに依存しない
  • メリット
    • 処理や出力を差し替えられる→画面なのかコンソールなのか変えられる
    • モックの差し替え、例外を起こすビジネスロジックの差し替えでバグ検証
  • 代償
    • レスポンスがView(MVC)の場合にマッチしない
    • めんどくさい
  • Scafold機能欲しい
  • まずはMVCを捨てる(!?)
    • プレゼンターで、どうにかしてViewを描画する
    • 戻り値をなくす Laravelだとミドルウェア機能で。
  • プラグイン作りました

知見のない技術スタックをプロダクション導入するエンジニアの導入戦略

資料: https://speakerdeck.com/hgsgtk/a-strategy-to-choice-no-knowledge-technology

  • 技術選定の視点とは?
    • 用件によって重視すべき点が変わる
    • 運用実績がないものの方がマッチするケースがある
  • 品質には外部品質と内部品質がある
    • ユーザーに見えるか見えないか 可用性があるか
    • 外部と内部でアプローチを考える
  • 業務でやったことないと分からん
    • いかに多く学びすぐに反映するか
  • じゃあどうやって学ぶの?
    • 学びのサイクルを観察する
    • テストコードを設計道具として使おう
  • 早期にデプロイして監視しておく
    • バグに気づく

脆弱性から学ぶWebセキュリティ

資料: https://speakerdeck.com/hypermkt/study-web-security-from-vulnerability2

  • 脆弱性の対策方法
    • 根本的解決 完全根絶
    • 保険的対策 影響範囲を限定させる
  • ディレクトリトラバーサル
    • ファイルパスをパラメータで受け付けない
    • バリデーションする
  • OSコマンドインジェクション
    • セミコロンを利用してLinuxコマンドが連続して実行される
    • 怪しい関数 -> system exec passthru shell_exec popen
    • escapeshellargでエスケープする
  • セッション管理の不備
    • セッションハイジャック
    • セッションIDの固定化
    • セッションアダプション セッションIDを有効なセッションIDとして許可してもらうこと
    • 認証が成功したら新しいセッションを開始する

オニギリペイのセキュリティ事故に学ぶ安全なサービスの構築法

資料: https://www.slideshare.net/ockeghem/phpconf2019

  • キャンペーン21%還元! -> 景品表示法違反で消費庁から呼び出し
    • 20%にして解決 -> キャンペーン取り消しで不信感を与えてしまった
  • パスワードスプレー攻撃で不正ログイン相次ぐ
    • IoT機器からの攻撃 -> 多数のIPアドレスからの分散攻撃
    • ユーザーIDを連番にしない、二段階認証をおこなう
    • 秘密の質問は禁止
    • パスワードの定期的変更の強制を禁止
  • 2段階認証の実装不備で不正ログインが止まらない
    • バイパスできた -> セッションIDにユーザーIDをセットしていた
    • 未ログイン/2段階認証待ち/ログイン済み でセッションIDを作成
  • ヘルプデスクにパスワードリセットされてログインできない
    • 仮パスワードを登録済みのメールアドレスに送信する
  • スマホアプリの端末上にパスワードが平文で保存されていた
    • iOS: KeyChainに保存する
    • Android: KeyStoreに保存する
  • リジェクト警告を無視してアプリがストアから消えた
    • 「Android」という単語をiOSアプリ内で使用してしまった
  • OSコマンドインジェクションで不正アクセスされた
    • 改竄検知で早期に発見された
    • エスケープが漏れていた
    • PHP7.4以降はproc_open関数で対策 シェル経由にならないため安全
  • WAFのリバースプロキシがオープンになってAWSのIAMが漏洩した
    • SSRF脆弱性という
    • Apacheのmod_securityを使っていた -> オープンリバースプロキシになっていた
  • リスクアセスメントの方法
    • ベースラインアプローチ 例:徳丸本に習ってやりましょう
    • 非形式的アプローチ 例:徳丸さんに来てもらいましょう
    • 詳細リスク分析 ベースラインで捉えられないものを考えましょう
  • 業務レベルでの脅威分析表を作れるかどうか
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WordPressのwpForo Forum(掲示板プラグイン)を解析して旧サイトから移行してみた

1.はじめに

もともと20年ほど旧サイト「らららのプログラマーズラウンジ」を運営していたのですが、XServer,WordPressのサイトへ引っ越ししました。これを機に(いまどき?になりますが)CGIの掲示板からWordPressのwpForo Forum(掲示板)へ移行しました。

(移行先は、こちらです。6万件弱の書き込みが移行されたことが確認できます)

top.png

 旧サイトは約20年ほどの掲示板書き込みデータがあり、もちろんそのデータを移行する必要があるのですが
 標準で移行方法が用意されておらず、自力で「wpForo Forum」を解析して移行しました。
 ゆうて掲示板だし「眺めれば、なんとかなるっしょ♪」と軽いノリでやってみた。
 と、そういう記事です。

 ちなみに、Qiitaでちょっと「wpForo」を検索してみたところ1件もヒットしない!
 WordPressはあってもwpForoは1件もないかw ということで需要はないのはわかりましたが、
 私のように(あまりいない)掲示板移行したい方向け、そして自分用のメモとして記事にしておきます。

2.移行元と移行先の掲示板について

掲示板(移行元)

移行元の掲示板は、「とほほのWWW入門」で配布されている「WwwLounge - CGI質問掲示板」です。
約20年ほど前に配布され、今は更新されていないCGI掲示板です。
当時は設置している方もちらほら見られて、知っている人は知っている掲示板でした。
 
書き込みデータは、ほんとテキストのログです。
データはこんな感じです。

Subject: WEB質問ラウンジ設置!
========================================
From: ららら
E-Mail: rararahp@cool.ne.jp
HomePage: http://rararahp.cool.ne.jp
Date: 2002/06/01(土) 02:28:34

こんにちは。らららです。

WEB用ラウンジを設置しました。
みなさん、ご活用くださいませ。m(_ _)m
========================================
From: xxx
Date: 2002/06/03(月) 12:36:42

新規ラウンジ解説、おめでとうございます。
ところで、ASPの設置予定はないのでしょうか?
========================================
From: xxx
Date: 2002/06/03(月) 17:34:41

こんにちは。ASPはHP作成に関わることなので、
このラウンジでよいんじゃないですかねぇ。

掲示板(移行先)

移行先の掲示板は、冒頭で書いたようにWordPressのプラグイン「wpForo Forum」です。
「wpForo」はWordPressの掲示板プラグインの中でも優れている機能を持っていて実は、他の掲示板からの移行ツールが用意されています。

migrationtool.png
 画像
 https://wpforo.com/docs/root/migrate-to-wpforo/

しかし「phpBB」や「MyBB」等の有名どころからの移行ツールは用意されているのですが
むかーしの日本のローカルでしか使われていなかった「WwwLounge」からの移行ツールはもちろん用意されていません。

しゅーりょーである?。。。

いや。待て待て。わいエンジニア!?
ちょっと眺めれば、掲示板データの移行なんて楽々いけるでしょ!
ということでやってみた。

3.wpForoの解析(完全に理解した。してない)

では、移行に向けてwpForの解析を始めます。
と言っても眺めるだけです?

wpForoにはドキュメント(英語)はあるのですが、
ぱっと見、移行ツール以外に有用な情報はなさげ。

フォーラムで書き込まれたテキストはどこにいくか?
WwwLoungeみたいにテキストファイルってことはなくデータベースでしょう。
ということで、MySQL管理ツールのphpmyadminからデータベースを覗くとそれっぽいものがありました。
table.png

名前が「wp_wpforo_」から続くテーブルがそうですね。

次にテーブルの定義やデータを眺めます。
(眺めるというのは、テーブル名前、項目名から意味を想定して、実際に掲示板から書き込みをしてみて、どのようなデータが生成されるかを確認する地道な作業。。。?)

と、すると移行にはこのあたりのテーブルをなんとかすれば良いことがわかります。

wp_wpforo_forums:各掲示板の情報
wp_wpforo_topics:投稿(ヘッダ)
wp_wpforo_posts:投稿(明細)
wp_wpforo_tags:タグ
wp_users:ユーザー (WordPressと共通)
wp_wpforo_profiles:ユーザーのプロファイル

wp_wpforo_topicsとwp_wpforo_postsが書き込みデータで
スレ主がスレッドを立てるとwp_wpforo_topics、wp_wpforo_postsがそれぞれ1件出来て、
レスをつけるとwp_wpforo_postsが1件づつ増えていく関係です。

それぞれの項目の意味はこんな感じです。
・wp_wpforo_topicsテーブル
・wp_wpforo_postsテーブル
tableddl.png
※必要なテーブルの項目は、すべてどのような意味か、データ値はどんな形式で何が入るかを洗い出します。掲示板で使っていない機能とかがあるとデータに変化が現れずわからない事もあるのですが、大体解析しました。

4.移行方針決定

移行方針は、事前にWordPressからサブフォーラムの作成など大まかな掲示板の設定を行い、
移行元掲示板からデータを抽出して、移行先掲示板にインポートしてあげます。これだけで動くはず!

移行元掲示板(WwwLounge)からのデータエクスポート

上記にも書きましたが、投稿はテキストファイルなので、このテキストファイルを解析して読み込むプログラムを自作します。
SubjectやDateなど特定キーワードや規則的な順序性があるので、これに基づいて読み込みます。

移行元→移行先へデータを変換

移行元からのエクスポートをしたデータをwpForoの形式に変換するプログラムを自作します。
(どのように変換するかは「wpForoの解析」のところで把握した構造になるように変換します)

★実際の抽出、変換プログラムはこちら
https://github.com/rararalounge/LoungeTool

移行先掲示板(wpForo Forum)へのデータインポート

インポートには、phpmyadminにインポート機能があるのでこれを使います。
以下のようにインポートするファイルのフォーマットを指定できます。
今回は、CSVにしています。
(XMLのほうが後々良いかなと思いましたが、1回インポート出来ればいいしCSVのほうがとりあえずなら簡単かなと。安易な選択)

またCSV読み込み時にやっかいな、カラムの区切り文字や囲み記号エスケープ記号などをインポート画面から指定できます。便利!
インポート.png

5.移行実行

ここまでくれば、あとはインポートして「めでたしめでたし!」のはずなんですが、実際インポートすると以下のようにエラーが出たり、インポートが成功しても想定していなかった問題が発生したりとです。
error.png

解析があまかったり、データが思うように出来ていなかったりといった事が原因ですね。トライアンドエラーを繰り返すしかねぇ!
いくつか対応したのですが、覚えているのだと以下のような問題が出ました。
(もっと問題が出た気がしますが、忘れてしまいました)

問題1

問題1 Q&AのURLがなぜか重複する

wpForoの仕様で、質問をすると以下のようにタイトルがそのままURLに使われます。
(例)
https://rarara.org/community/programming/コードで作成したコントロールのイベントハンドラー

ただこのURLの長さに最大制限があって途中で切り取られます。普通に使う分には重複すると連番(ハイフンと数字)が付与される仕様なので問題ないのですが、私のように移行プログラムでインポートしようとしたところユニークチェックにひっかかって大量にエラーが出ました。

タイトルに
「コードで作成したコントロールのイベントハンドラー1」
「コードで作成したコントロールのイベントハンドラー2」
「コードで作成したコントロールのイベントハンドラー3」
というように連番をついている場合や
「コードで作成したコントロールのイベントハンドラーでエラー発生」
というようにタイトルが途中までが同じ場合です。。。

これは変換プログラムを修正して、重複したらURLに連番をつけるようにして対応しました。

問題2

問題2 検索文字列をURLエンコードしてくれない

これはwpForoのバグなのですが、例えばサイドバーからタグ「C#」と検索しても#がURLエンコードされておらず正しく検索出来ない。

公式サイトへの修正依頼

とりあえずwpForoはPHPで出来ているのでソースコードを眺めて、修正箇所を見つけて対応しました。ただプラグインがバージョンアップした際に自分が行った修正が消えてしまうのが面倒だなぁと思ったので、wpForoの公式にサポートがあるので以下のように修正依頼を行いました。

依頼.png

返答はその日のうちにありました。はやい!
回答としては、「そのバグは知っている。次のバージョンで修正する予定」とのこと。

おまけに

移行先の掲示板では、ユーザー登録制(登録しなくても投稿と書き込みは可能)なのですが、ユーザー登録していると投稿数に応じて以下のように貢献度がわかるちょっとだけうれしい仕様です。
profile.png

昔から書き込んでくれているユーザーと、移行した掲示板ログをひもづけるために以下のように直にSQLでデータをいじって対応したりしています。(ユーザー登録して頂いた方のみですが)

UPDATE `wp_wpforo_topics` SET name = "", email="", userid = ユーザーID WHERE name = "ユーザー名"
UPDATE `wp_wpforo_posts` SET name = "", email="", userid = ユーザーID WHERE name = "ユーザー名"

(上記のSQLは、過去のユーザー名(文字列)で検索して登録したユーザーへの置き換え)
※一応、解析でテーブル構造を把握しているからこそ、こんな対応もできる。

6.さいごに

エンジニアだから、存在しないんだったら(今回は移行ツール)自分で作れば良いですね。
エンジニアとかたいそうなものじゃなくて、今回のようにどのように解決するかの思考プログラムがちょっと書けるぐらいで大丈夫!

あと、移行先の「らららのエンジニアラウンジ
移行前よりさらにアクセスが減っているような。。。引っ越ししたばかりで検索にヒットしにくいというのはあるかもしれませんが。
いまどき掲示板は難しい(チャットやTwitter等と比べて)のはわかっておりますが、みなさんのノウハウが参照できる形でずっと残るという意味ではとても有用だとと思っています。現に約20年分の技術Q&Aのログが本サイトには残っています。

ということで当サイトにアクセスして頂けると喜びます!???

また私としてはQiita初投稿でした。勉強になりました。そしてこのしくみ便利だぁー。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

カンファレンスに初めて行ってみた

はじめに

2019年12月1日にあったphpカンファレンスに行った。

https://phpcon.php.gr.jp/2019/

恥ずかしながら、エンジニア4年目にしてまだカンファレンスとか行ったことなかったため、ちょっと緊張しつつも、参加したので、

  • 初めて勉強会とかカンファレンスに行く予定の人
  • 行ったことないけど、行ってみたい人

向けになればと思い、ちょこっと書く。

カンファレンスとは

要は勉強会。大学の特別講義をイメージしてもらうのがいいかも。

今回のphpカンファレンスの場合、phpに関すること、開発環境について、システム設計に関わることなどphp関連のあれこれをテーマにした講演がある。

また、企業でブースを出しているところもあり、各企業のところに行けば、登壇した人からお話を聞くことも可能(っぽい。今回自分は用事があったため、行かなかった。)

行ってよかったこと

刺激になる

自分より全然できる人たちが、自分に向けて話してくれるので、勉強になる。また、自分の周りにいるエンジニアたちと話していると、どうしてもその世界に凝り固まってしまう。世界にはまだまだエンジニアはたくさんいるし、新しい考えを入れることで、勉強になる。

他の人も同じことで悩んでいて安心する

自分たちが立ち向かってる課題は他の企業でも同じことになってるんだなぁと思える。
その中でこんなことやってきました!と聞けるので、うちでもやってみようとなるんじゃないかなぁ。

なんか色々もらえる

各企業がある程度採用目的でブースを出展しているところもあるため、なんか色々もらえる機会が多い。

準備しておいたほうがいいもの

  • メモ:どういう風にメモをするかはわからないが、PCでも紙でもいいので、持っているほうが何かと便利。
  • 荷物を入れる何か:なんだかんだ色々もらえるので、それをしまえるものがあるといいかも。
  • Twitterアカウント:講演した資料とかをTwitterで公開してくれるから、あとから見る為にも、公演中に手元で資料見る為にもあると便利。

おわりに

普段ただプログラミングをやっていると手に入れられないこととかを勉強できるので、機会を見つけて、参加してそれをまた発信するというをやってったほうがよさそうだなぁ

実際に聞いた講演はまた別の記事で。

①MVCとはなにか?
https://qiita.com/chanyone_0505/items/9b3c5cd9c410a1406a16

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPでREPLを使うにはphp -a

実際の動作

ターミナル
~ $ php -a
Interactive shell

php > $var = "JavaScript";
php > echo $var;
JavaScript

対話形式で実行結果を見られる

デバッグとか、ちょっとしたサンプルコードの動きを見るとき
いちいちphpファイルに書くのは面倒ですよね。
そういう時、このようにすればすぐに結果が見られます。

PHPでは使っている人が少ない?

あまりPHPでREPLを使っている人は多く無い印象。
私の知識不足かもしれませんが。
便利なので非常にオススメです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【もう悩まない!】WordPressのexcerptやtitle文字数操作の奥義をお教えしますよ

みなさんwordpressで開発をしていて、よくタイトルや抜粋文の文字数制限や、末尾を...にしたりしますよね。

そしてやり方をググると、例えば抜粋文なら大抵このようなコードを目にするかと思います。

function twpp_change_excerpt_length( $length ) {
  $length = 50;

  if ( is_category() || is_tax() ) {
    $length = 100;
  } 

  return $length; 
}

add_filter( 'excerpt_length', 'twpp_change_excerpt_length', 999 );

これはページごとに抜粋文の長さを変えられるコードですね。
excerpt_lengthフィルターを使っています。

function twpp_change_excerpt_more( $more ) {
  return '......';
}
add_filter( 'excerpt_more', 'twpp_change_excerpt_more' );

これは、抜粋文の省略記号を...にするよくあるやつです。

excerpt_moreフィルターを使っています。

一部分だけ適用したいんだが...

そう、例えばページの人気記事一覧の部分だけ文字数を20文字、そのほかの記事一覧の文字数を50文字にしたい場合どうでしょうか。

そういう風に融通がきくコードを今回紹介します。

まずはタイトルの制御から

タイトル文の制御は抜粋文に比べて楽なので、肩慣らしと行きましょう。

20文時までに制限して、それを超えたら末尾は...にします。

いきなりコードを見ます。

<?php
if(mb_strlen($post->post_title)>20) {
  $title= mb_substr($post->post_title,0,20) ;
    echo $title . '...';
  } else {
    echo $post->post_title;
  }
?>

mb_srtlenとmb_substr

mb_strlen ( string $str [, string $encoding = mb_internal_encoding() ] ) : int
mb_substr ( string $str , int $start [, int $length = NULL [, string $encoding = mb_internal_encoding() ]] ) : string

肝はこの辺ですね。

上から順に見ていくと、

まず$post->post_titleでタイトルを投稿から文字列で持ってきてます。

そしてそれをmb_strlenで文字数調査、今回は20文字制限にしてます。

20文字以上ならmb_substrで文字数制限をかけます。

プラグインのWP Multibyte Patchを入れていること

そしたらechoでそのタイトルと、末尾に...を出力します。

ここでif文分岐が起きているのは、if文で文字数調査をしないと、20文字以内のタイトルの末尾にも...がついてしまうためですね。

else以下は当然20文字以下なので、タイトルのみechoします。

抜粋文(excerpt)はやっかい

それでは抜粋文を見ていきましょう。

20文字に制限して、それ以上のときは...を出力します。

コードを見ます。

<?php if (mb_strlen($post->post_content, 'UTF-8') > 20) { 
$content = str_replace('\n', '',
                       mb_substr(strip_tags($post->post_content), 0, 20, 'UTF-8'));
echo $content . '...';
} else {
echo str_replace('\n', '', strip_tags($post->post_content));
}
?>

『やだいきなり複雑になった怖いやめてください?』

落ち着いて。calm down。1つ1つ解説します。

str_replaceとstrip_tags

str_replace( $検索文字列 , $置換後文字列 , $検索対象文字列 [, int &$count ] )

基本はタイトル文の制御と一緒ですが、post_contentにはhtmlタグが含まれています。

そのためそれらを取り除ければ良さそうです。

まずはmb_strlenで文字数調査をします。

mb_substrで文字数制限をする前に、strip_tagsでhtmlタグを取り除きます。

最後にstr_replaceで半角スペースを取り除いて純粋な文字列になったものを$contentに代入して終わりです。

全体に適用するか、個別に適用するか

ここまで紹介したコードは、どちらかといえば細かに適応させたい場合に有効かなと思います

ページ全体でタイトルや抜粋文を制御したい場合はfunctions.phpの方がいいと思いますし、同じページ内で場所ごとに文字数制御を変えたいなら、今回のコードは使えるかなと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPカンファレンス2019基調講演まとめ

qnote アドベントカレンダー第1日目は、ちょうど PHP カンファレンス2019の開催日という事もあり、現地からの採れたてレポートをお送りしたいと思います。

8ADF5D24-FCAC-4401-B272-D6E233105B4C.jpeg
採れたての elePHPant (3,000円) 。経費で落ちる?

PHPカンファレンス2019

今年はいつもよりも遅めの12月開催。ちょうど11月29日にPHP7.4がリリースされ、またPHP7.1が EOL を迎えた日でもあります。
毎年おなじみの大田区産業プラザにて19年目20回目の開催。

8B5C7269-F8AE-479E-AAAC-C26EFC14C8B9.jpeg

今年は嬉しいことに後輩 (@kambe0331) がスタッフとして参加。きっと刺激をうけて来年は登壇してくれる事を期待しています。

基調講演

乗り換え間違えてギリギリ到着。まずは御目当てのキーノートを拝聴。
1回目から参加している私としては廣川さんのキーノートを毎回楽しみにしています。
年を重ねるごとに廣川さんがティムクックに見えるのは多分私だけではないかと思いますが、なんと今は宇宙開発に携わっているそうです。すごい!

B4BA156B-303C-413E-AC48-42B7177A189A.jpeg

キーノートではPHPのこれまでとこれから、また、PHP7.4の仕様変更などをわかりやすく紹介していただきました。30分くらいの時間で内容は濃いのに非常にまとまっていてこの辺りはさすがベテランの魅せる技という感じでした。

PHP7.4 改善/変更のポイント

キーノートにもありましたが簡単にまとめます。

クラスのプロパティ型指定

PHP7から導入されたメソッド引数と返り値のタイプヒンティングのプロパティ版。
Nullable 識別子(オプショナル型みたいなもの?)で null を許容するかどうかも定義可能。

class Foo {
    public int $a;
    public ?object $b = null;
}

後方互換のためか、デフォルトでは $a に string を突っ込んでもエラーにはならず、int にキャストするだけというのは PHP らしいですが、

declare(strict_types=1);

と書く事で Fatal error を出す事が可能。

他の言語では当たり前ですが、ようやく PHP でもタイプセーフなコードが書けるようになりそうですね。

OpCache のプリロード指定

Webリクエストで実行されたコンパイルのキャッシュはこれまでもありましたが、あくまでも起動後に実行されたコードのキャッシュなので、再起動直後はキャッシュ生成のオーバヘッドがありました。これをオプション指定で起動時にキャッシュしてしまおうというもの。これは php.ini に書けば良いのかな?

opcache.preload=/var/www/preload.inc

FFI (Foreign Function Interface)

あまり知見が無いですが(←調べろ)、PHPからCのソースを実行するようなやつ?前からエクステンションにあったものをネイティブに移植する事によって、前述のプリロード指定などで高速化し実現に至ったようです。より高速な処理が求められる場合に有効。

$ffi = FFI::cdef(double atan2(const double, const double);, libm.so.6)
var_dump($ffi->atan2(0.1, 0.0);

昔あったJavaのブリッジエクステンションみたいなもんかな?

配列スプレッド構文

JavaScript ではお馴染みの構文ですね。これまで array_merge を使用していた部分がより簡潔に書けます。

$a = [3, 4];
$b = [1, 2, ...$a, 5]; // => [1, 2, 3, 4, 5];

アロー関数 (ショートクロージャー)

これも JavaScript ぽいですね。個人的には fn はダサいなと思いますが。。
これが、

function ($x) use ($y) {
   return ($x * $x + $y);
}

こう書けます。

fn($x) => ($x * $x + $y);

親スコープの変数が参照できる(つまりuse()が不要)なのでより簡潔になりますね。

??=構文

NULL合体演算子をより実用的にしたもの。変数が設定されていない場合に代替文字列を代入。

// PHP7
$a[user] = $a[user] ?? nobody;

// PHP7.4
$a[user] ??= nobody;

で、これ、何演算子っていうの?

その他

これ以外にも細かな関数の追加や演算子の仕様変更、弱参照のインタフェース追加など、
PHP8のリリースまでの最後のメジャーバージョンという事もあり、新機能盛り沢山でした。早く仕事で使いたい!

まとめ

そんなわけで基調講演を堪能し、後輩が働いているところも確認できたので、カフェに待避し iPad で Qiita のこの原稿を書いています。午後もいろいろ楽しみなセッションがあるので、年に一度のお祭りを楽しんできます!

qnote のアドベントカレンダー (https://qiita.com/advent-calendar/2019/qnote) も PHP だけでなくいろんな言語、技術のお話をお届けできるかと思いますのでどうか皆さんよろしくお願いいたします!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【レポート】PHPカンファレンス2019キーノート

qnote アドベントカレンダー第1日目は、ちょうど PHP カンファレンス2019の開催日という事もあり、現地からの採れたてレポートをお送りしたいと思います。

8ADF5D24-FCAC-4401-B272-D6E233105B4C.jpeg
採れたての elePHPant (3,000円) 。経費で落ちる?

PHPカンファレンス2019

今年はいつもよりも遅めの12月開催。ちょうど11月29日にPHP7.4がリリースされ、またPHP7.1が EOL を迎えた日でもあります。
毎年おなじみの大田区産業プラザにて19年目20回目の開催。

8B5C7269-F8AE-479E-AAAC-C26EFC14C8B9.jpeg

今年は嬉しいことに後輩 (@kambe0331) がスタッフとして参加。きっと刺激をうけて来年は登壇してくれる事を期待しています。

基調講演

乗り換え間違えてギリギリ到着。まずは御目当てのキーノートを拝聴。
1回目から参加している私としては廣川さんのキーノートを毎回楽しみにしています。
年を重ねるごとに廣川さんがティムクックに見えるのは多分私だけではないかと思いますが、なんと今は宇宙開発に携わっているそうです。すごい!

B4BA156B-303C-413E-AC48-42B7177A189A.jpeg

キーノートではPHPのこれまでとこれから、また、PHP7.4の仕様変更などをわかりやすく紹介していただきました。30分くらいの時間で内容は濃いのに非常にまとまっていてこの辺りはさすがベテランの魅せる技という感じでした。

PHP7.4 改善/変更のポイント

キーノートにもありましたが簡単にまとめます。

クラスのプロパティ型指定

PHP7から導入されたメソッド引数と返り値のタイプヒンティングのプロパティ版。
Nullable 識別子(オプショナル型みたいなもの?)で null を許容するかどうかも定義可能。

class Foo {
    public int $a;
    public ?object $b = null;
}

後方互換のためか、デフォルトでは $a に string を突っ込んでもエラーにはならず、int にキャストするだけというのは PHP らしいですが、

declare(strict_types=1);

と書く事で Fatal error を出す事が可能。

他の言語では当たり前ですが、ようやく PHP でもタイプセーフなコードが書けるようになりそうですね。

OpCache のプリロード指定

Webリクエストで実行されたコンパイルのキャッシュはこれまでもありましたが、あくまでも起動後に実行されたコードのキャッシュなので、再起動直後はキャッシュ生成のオーバヘッドがありました。これをオプション指定で起動時にキャッシュしてしまおうというもの。これは php.ini に書けば良いのかな?

opcache.preload=/var/www/preload.inc

FFI (Foreign Function Interface)

あまり知見が無いですが(←調べろ)、PHPからCのソースを実行するようなやつ?前からエクステンションにあったものをネイティブに移植する事によって、前述のプリロード指定などで高速化し実現に至ったようです。より高速な処理が求められる場合に有効。

$ffi = FFI::cdef(double atan2(const double, const double);, libm.so.6)
var_dump($ffi->atan2(0.1, 0.0);

昔あったJavaのブリッジエクステンションみたいなもんかな?

配列スプレッド構文

JavaScript ではお馴染みの構文ですね。これまで array_merge を使用していた部分がより簡潔に書けます。

$a = [3, 4];
$b = [1, 2, ...$a, 5]; // => [1, 2, 3, 4, 5];

アロー関数 (ショートクロージャー)

これも JavaScript ぽいですね。個人的には fn はダサいなと思いますが。。
これが、

function ($x) use ($y) {
   return ($x * $x + $y);
}

こう書けます。

fn($x) => ($x * $x + $y);

親スコープの変数が参照できる(つまりuse()が不要)なのでより簡潔になりますね。

??=構文

NULL合体演算子をより実用的にしたもの。変数が設定されていない場合に代替文字列を代入。

// PHP7
$a[user] = $a[user] ?? nobody;

// PHP7.4
$a[user] ??= nobody;

で、これ、何演算子っていうの?

その他

これ以外にも細かな関数の追加や演算子の仕様変更、弱参照のインタフェース追加など、
PHP8のリリースまでの最後のメジャーバージョンという事もあり、新機能盛り沢山でした。早く仕事で使いたい!

まとめ

そんなわけで基調講演を堪能し、後輩が働いているところも確認できたので、カフェに待避し iPad で Qiita のこの原稿を書いています。午後もいろいろ楽しみなセッションがあるので、年に一度のお祭りを楽しんできます!

qnote のアドベントカレンダー (https://qiita.com/advent-calendar/2019/qnote) も PHP だけでなくいろんな言語、技術のお話をお届けできるかと思いますのでどうか皆さんよろしくお願いいたします!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPで分割代入していくだけの投稿

この記事は PHP Advent Calendar 2019 の1日目の記事です。

分割代入とは

配列の中身を複数の変数に一回で代入できる書き方

list()[]

list()を使うか、PHP7.1以降では[]を使ってもOK

この投稿では基本的に[]の書き方で試していきます。

$colors = ['red', 'yellow', 'blue'];

[$x, $y, $z] = $colors;
var_dump($x, $y, $z); // "red", "yellow", "blue"

list($a, $b, $c) = $colors;
var_dump($a, $b, $c); // "red", "yellow", "blue"

swapした例

$a = 'apple';
$b = 'banana';

[$a, $b] = [$b, $a];
var_dump($a, $b); // "banana" "apple"

list($a, $b) = [$b, $a];
var_dump($a, $b); // "apple" "banana"

// 関数を用意する例
// 参考:https://www.php.net/manual/en/migration71.new-features.php
function swap(&$a, &$b): void {
    [$a, $b] = [$b, $a];
}
swap($a, $b);
var_dump($a, $b); // "banana" "apple"

keyを指定してできる

list()でキーを指定するやり方はPHP7.1から

$animals = ['dog' => 'イヌ', 'cat' => 'ネコ'];

['dog' => $dog1, 'cat' => $cat1] = $animals;
var_dump($dog1, $cat1); // "イヌ" "ネコ"

list('dog' => $dog2, 'cat' => $cat2) = $animals;
var_dump($dog2, $cat2); // "イヌ" "ネコ"

注意点

一回はまった部分なのですが、配列をソートなどした場合に、インデックスを振り直さないと思い通りに動いてくれない時がありました

// 下2つの例は同じ意味で代入される
[$a, $b] = ['aaa', 'bbb'];
var_dump($a, $b); // "aaa", "bbb"
[$c, $d] = [0 => 'ccc', 1 => 'ddd'];
var_dump($c, $d); // "ccc", "ddd"

// 配列をソートしたがインデックスが変わってないなどの場合に、このような配列になったとすると
[$z, $y] = [1 => 'zzz', 0 => 'yyy'];
// $zに "yyy"、$yに "zzz" が代入された状態になってしまう!
var_dump($z, $y); // "yyy", "zzz"

// やり方はいろいろあると思いますが、
// array_values などで インデックスが振り直された状態にするとうまくいきました。
[$z, $y] = array_values([1 => 'zzz', 0 => 'yyy']);
var_dump($z, $y); // "zzz", "yyy"

代入する変数が少ない場合

[$a, $b] = [10, 20, 30, 40, 50];
var_dump($a, $b); // 10 , 20

[, $a, , , $b] = [10, 20, 30, 40, 50];
var_dump($a, $b); // 20 , 50

['blue' => $blue] = ['red' => '赤', 'blue' => '青'];
var_dump($blue); // 青

配列のキーが存在しない場合

[$a, $b] = [10];
var_dump($a, $b); // 10, NULL

[5 => $c] = [10]; // Notice: Undefined offset: 5

['a' => $aaa] = ['b' => 'bbb']; // Notice: Undefined index: a

ネストできる

[[$a, $b], [$c, $d]] = [[1, 2], [3, 4]];
var_dump($a, $b, $c, $d); // 1, 2, 3, 4
$config = ['db' => [
    'connection' => 'mysql',
    'host' => 'localhost',
    'database' => 'sample',
    'user' => 'root',
    'pass' => 'root',
]];

['db' => [
    'connection' => $connection,
    'host' => $host,
    'database' => $database,
    'user' => $user,
    'pass' => $pass,
]] = $config;

var_dump($connection, $host, $database, $user, $pass); // "mysql" "localhost" "sample" "root" "root"

foreach

$foods = [
    ['apple', 'tomato', 'cherry'],
    ['banana', 'orange', 'paprika'],
];
foreach($foods as [$food1, $food2, $food3]) {
    var_dump($food1, $food2, $food3);
}

$foods = [
    [
        ['apple', 'cherry'],
        ['tomato'],
    ],
    [
        ['banana', 'orange'],
        ['paprika'],
    ]
];
foreach($foods as [[$fruit1, $fruit2], [$vegetable1]]) {
    var_dump($fruit1, $fruit2, $vegetable1);
}
$todos = [
    [
        'id' => 1, 
        'is_done' => true, 
        'todo' => '買い物',
    ],
    [
        'id' => 2, 
        'is_done' => false, 
        'todo' => '映画',
    ],
];
foreach($todos as ['id' => $id, 'is_done' => $isDone, 'todo' => $todo]) {
    var_dump($id, $isDone, $todo);
}

$todos = [
    [
        'id' => 1, 
        'is_done' => true, 
        'todo' => [
            'shopping' => 'コンビニ',
        ],
    ],
    [
        'id' => 2, 
        'is_done' => true, 
        'todo' => [
            'shopping' => '百均',
        ],
    ],
];
foreach($todos as ['todo' => ['shopping' => $shopping]]) {
    var_dump($shopping);
}

配列の形でも代入できる

$todos = [
    [
        'id' => 1, 
        'is_done' => true, 
        'todo' => '買い物',
    ],
    [
        'id' => 2, 
        'is_done' => false, 
        'todo' => '映画',
    ],
];

$ids = [];
foreach($todos as $todo) {
    ['id' => $ids[]] = $todo;
}
var_dump($ids); // [1, 2]

クラスでも分割代入する

クラスで分割代入しようとすると、このようにエラーになるのですが、

class A {
    public $hoge = 'hoge';
}

['hoge' => $hoge] = new A; // PHP Fatal error:  Uncaught Error: Cannot use object of type A as array
var_dump($hoge);

ArrayAccessインターフェースを実装することで、クラスでも分割代入することができました。

参考:ArrayAccess,IteratorAggregateインタフェースで配列操作可能なオブジェクトを作成する - qiita

※↓実装の内容はLaravelのCollectionクラスをパクりました。

<?php

class A implements ArrayAccess
{
    private $items = [];

    public function __construct(array $values = []) {
        $this->items = $values;
    }

    public function offsetExists($offset)
    {
        return array_key_exists($offset, $this->items);
    }

    public function offsetGet($offset)
    {
        return $this->items[$offset];
    }

    public function offsetSet($offset, $value)
    {
        if (is_null($offset)) {
            $this->items[] = $value;
        } else {
            $this->items[$offset] = $value;
        }
    }

    public function offsetUnset($offset)
    {
        unset($this->items[$offset]);
    }
}

// ArrayAccessインターフェースを実装すると、クラスを配列のように扱うことができる
$A1 = new A;
$A1[0] = 'hoge';
var_dump($A1[0]); // "hoge"

// 分割代入もできました!
$A2 = new A(['a', 'b', 'c']);
[$a, $b, $c] = $A2;
var_dump($a, $b, $c); // "a" "b" "c"

$A3 = new A(['dog' => '犬', 'cat' => '猫', 'mouse' => 'ネズミ']);
['dog' => $dog, 'cat' => $cat, 'mouse' => $mouse] = $A3;
var_dump($dog, $cat, $mouse); // "犬" "猫" "ネズミ"

参考


最後まで見ていただいてありがとうございました。m(_ _)m

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む