20190828のPHPに関する記事は20件です。

xampp + VScode でWebサービス開発環境構築

はじめに

サークルでphpを使ってWebサービスを作りました。
後輩に引継ぎをする時期となったのでWebサービス環境構築方法をここに掲載しておきます。
phpを使用せずにWebサービスを開発する場合でも、ローカルで動作を確認するためのツールを導入するということで参考になるかと思います。

導入環境

Windows10 (64bit)

概要

  1. Visual Studio Code (VScode) のインストール
  2. xampp のインストール
  3. パスを通す
  4. 動作確認

手順

VScode

インストール

VScodeのインストールに際しては以下のサイトが参考になったので、ここでの説明は割愛させていただきます。
https://www.kkaneko.jp/tools/win/vscode.html

今回はダウンロードとインストールまで済んでいればOKです。

VScodeの拡張機能のインストール

  1. メニューバーの表示(View)を選択し、拡張機能(Extensions)を押します
  2. またはCtr + Shift + Xを押します。
  3. 検索でphp serverを押し、PHP Serverを探します(画像のもの) php.png
  4. installを押して完了です

xampp

またまた他のサイトが参考になりましたのでここでの説明は割愛させていただきます。
https://qiita.com/minuro/items/d7f2b95b922ae302577c

xamppのコントロールパネル(以下の画像)が出てくるところまででOKです
xampp.png

パス

これでラストです。ちょっとややこしいので注意して見てください!
1. PCの検索等で環境変数と打って画像のような画面が出てきたら、下の環境変数を押します
php.png
2. システム環境変数Pathを選択して編集を押します
3. 環境変数名の編集という画面に移るので、新規を押してC:\xampp\phpと記入します(プライバシーの都合上黒塗りしています)
無題.png
4. あとは、OKを押してPCを再起動するだけです

動作確認

適当なところでいいので、
フォルダ:test
ファイル名:index.php
を作成し、index.phpには以下のコードをコピペしてください

<!-- index.php -->

<!DOCTYPE html>
<html lang="ja-JP">
<head>
    <meta charset="UTF-8">
    <title>テスト</title>
</head>
<body>
    これはテストです。以下の文章はphpで出力されています<br>
    <?php
        echo "これはphpです";
    ?>
</body>

次に、VScodeを起動します。
メニューバーからファイルを選択してフォルダーを開くで今作ったtestフォルダを開きます。
index.phpを開き、コードの上で右クリックし、PHP Server: Serve Projectを押します。
すると、ブラウザが立ち上がり、このような画面がでるはずです。
無題.png

注意

  • フォルダから開かずにファイル単体(ここだとindex.php単体)でVScodeで開くとエラーが起きるので注意してください。必ずフォルダから開くようにしてください
  • また、フォルダの直下には必ずindexと名前の付いたファイルをだた一つ作成するようにしてください。

最後に

ここまでできていれば、Webサービス開発環境は整っています!
正確にはローカル開発環境ですが...

今回はindex.phpで確認しましたがindex.htmlであっても問題はありません。
(自身、HTML, CSS, JSのみでサービスを作る際にここで作成した開発環境を利用しました)

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

xdebugパラメータメモ

xdebug.remote_autostart

xdebug.remote_autostart=1 にすると、Cookieに XDEBUG_SESSION を付与しなくてもデバッグセッションが始まる(ブレークするようになる)。

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

xdebugメモ

xdebug.remote_autostart

xdebug.remote_autostart=1 にすると、Cookieに XDEBUG_SESSION を付与しなくてもデバッグセッションが始まる(ブレークするようになる)。

Linuxにxdebugを入れる時の手順

git clone https://github.com/xdebug/xdebug.git -b 2.7.2
cd xdebug
./rebuild

で、modules 内にビルドしたxdebug.soが生成される。

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

Laravel 5.8 任意に設定したカラムを外部キーとする場合のリレーション

変則的なテーブル設計のリレーションを定義する機会があり、調べるのに時間がかかったためメモります。

今回使うテーブルの設計

1つのwork(仕事)に対して複数のapplicaiton(応募)が存在する一対多の関係。これら2つのテーブルをwork_idでリレーションして簡単に呼び出せるようにする。

特筆すべき点
・workテーブルの主キーがidではなくwork_id
・work_idには32桁のuuidが入る。typeはstring

スクリーンショット 2019-08-28 22.03.24.png

環境

OS : macOS Mojave 10.14.5
PHP : 7.3.1
mysql : Ver 14.14 Distrib 5.6.42, for osx10.14 (x86_64) using EditLine wrapper
Laravel : 5.7.6

手順

・migration
・Workの主キーの設定
・リレーションの設定をモデルに記述

migration

workテーブル

create_works_table.php
public function up()
    {
        Schema::create('works', function (Blueprint $table) {
            $table->char('work_id',32);
            $table->primary('work_id');
            //work_idを主キーに設定
            $table->string('catch_copy',50);
            $table->string('place',50);
            $table->string('description',500);
            $table->timestamps();

        });
    }

applicationテーブル

create_applications_table.php
public function up()
    {
        Schema::create('applications', function (Blueprint $table) {
            $table->bigIncrements('id');

            $table->char('work_id',32);
            $table->foreign('work_id')->references('work_id')->on('works');
            // このテーブルのwork_idを、worksテーブルのwork_idを参照する外部キーにする

            $table->string('message',500);
            $table->timestamps();
        });
    }

Workの主キーの設定

workの主キーの名前はidではなく、かつuuidであります。laravel標準のものとは違うため少し設定が必要となります。

Work.php
class Work extends Model
{
    protected $primaryKey = 'work_id';
    // 主キーをオーバーライド
    public $incrementing = false;
    // IDが自動増分されない場合
    protected $keyType = 'string'
    // 主キーが整数でない場合
}

Laravelの主キーについての設定についてはドキュメントの以下のページに記載されています。
Eloquentの準備(https://readouble.com/laravel/5.8/ja/eloquent.html)

$primaryKey

各モデルの親クラスであるModel.phpを確認すると以下のように'id'がデフォルトとなっています。

Model.php
protected $primaryKey = 'id';

$primaryKeyをWork.phpでオーバーライドしてあげることで、Workの主キーをwork_idに設定することができます。

$incrementing

$incrementingは、IDが自動増分されるかどうかのプロパティです。Model.phpではtrueとなっています。
今回はuuidゆえ自動増分されないので、falseにしてあげます。

$keyType

$keyTypeは、主キーtypeについてのプロパティです。
Model.phpではintとなっています。整数ではない場合、stringに設定します。

(incrementing、keyTypeの設定はなくても動きました。書かないとどんな時に困るのか教えて欲しいです。)

リレーションの設定をモデルに記述

まずは先ほどのWork.phpに書き加えます

Work.php
class Work extends Model
{
    protected $primaryKey = 'work_id';
    public $incrementing = false;
    protected $keyType = 'string';

    public function applications(){
        return $this->hasMany('App\Application', 'work_id');
        // 第二引数にforeign_keyを設定する。
    }
}

次はApplication.phpです。

Application.php
class Application extends Model
{
    public function work(){
        return $this->belongsTo('App\Work','work_id');
        // 第二引数にforeign_keyを設定する。
    }
}

ドキュメントは以下になります。
リレーション(https://readouble.com/laravel/5.8/ja/eloquent-relationships.html)

foreign_key

foreign_keyは引数で指定しない場合、モデル名 +_+ $primary_keyという形になります。

先ほど、workの$primary_keyをオーバーライドしたため、'work_work_id'となってしまっています。これではエラーが起きてしまうため、今度はforeign_keyをオーバーライドしてあげましょう。

参考

hasMany,hasOne,belongsToなどのリレーション関連のメソッドは
Illuminate\Database\Eloquent\Concerns\HasRelationships.phpに記述されています。

HasRelationships.php
public function hasOne($related, $foreignKey = null, $localKey = null)
    {
        $instance = $this->newRelatedInstance($related);
        $foreignKey = $foreignKey ?: $this->getForeignKey();
        $localKey = $localKey ?: $this->getKeyName();

        return $this->newHasOne($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey);
    }

local_key

今回は指定しませんでしたが、foreign_keyだけではなく、local_keyも指定することができます。

local_keyはデフォルトだと、primary_keyになります。今回はもうすでに$primary_keyをwork_idとオーバーライドしたため新たな設定は必要ありませんでした。

もし、Work.phpで$primary_keyをオーバーライドしなかった場合は、local_keyつまり第三引数にwork_idを入れてあげると動きました。

終わりに

ネットでもググってもあんま情報探せなかったけど、ドキュメントを見たら書いてあった。いい経験になった気がする。
変なところあったら優しく教えてください。お願いします。

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

PHP関係まとめ

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

PHPにおけるマスクありIPアドレスのバリデーション

言い訳

同じことしてる人は百万人いると思いますが、検索した時に、
IPアドレス単体のバリデーションが主で、マスクありのバリデーションが出てこなかったので。
備忘録です。

やりたいこと

192.168.0.1/24
192.168.0.1/32

クライアントからマスクありのIPアドレスを受け取った際、異常な値かどうかを確認すること。

コード

function ip_check( $value ) {
    if ( strpos( $value, '/' ) === false ) {
        return false;
    }
    list( $ip, $mask ) = explode( '/', $value );
    if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
        return false;
    }
    if( $mask > 32 || $mask < 1 ) {
        return false;
    }

    return true;
}

判定する時はこちら

IPアドレスが指定した範囲内にあるかどうか判別する
https://qiita.com/ran/items/039706c93a8ff85a011a

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

PhpStormとxdebugがどういう関係なのかのまとめ

最初にまず見るべき

いきなり人様の記事のリンク張るのもあれだけど、まずは↓を見る。
[PHP] Xdebug のリモートデバッグ、理解していますか?

PhpStorm側の話

デバッグを開始する際に受話器のアイコン↓をオンにするが、xdebugがデバッグ情報を送ってくる受け口を立てている。
image.png

PhpStormの設定(settings > Languages&Frameworks > PHP > Debug > xdebugの欄)にあるのも、"自分自身が何番ポートでデバッグ情報を受けるのか" という設定となる。

ちなみに、xdebugからの初回アクセスがあった際に、Incoming connectionのダイアログがでて、Accept する必要があるが、Acceptすると、settings > Languages&Frameworks > PHP > Servers に設定が勝手に作られる。

xdebug側の話

php.iniのxdebugセクションに設定する↓はどこにデバッグ情報を送るかという設定になる。

xdebug.remote_host=127.0.0.1
xdebug.remote_port=9000

xdebugの公式 内の↓も要参照。

  • "With a static IP/single developer"
  • "HTTP Debug Sessions"

xdebugのパラメータの話は ココ も参照。

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

【中級者向け】Laravelパッケージ開発ハンズオン

Laravelのパッケージ開発を行う流れやディレクトリ構成について考えました。

流れ

  • 作業ディレクトリを作成
  • 開発パッケージを作成
    • 例として artisan に hello:world コマンドを追加する
  • Docker環境の構築
  • 開発パッケージのインストール
  • 動作確認

最終的なディレクトリ構成

.
└── work # 作業ディレクトリ
    ├── core # Laravelインストール場所
    ├── docker # docker環境
    └── packages # パッケージのディレクトリ
        └── laravel-hello-world # myパッケージ
  • 複数のパッケージを開発する場合を考慮して packages と一つディレクトリを挟む
  • coreにLaravelをインストールする。
  • Laravelのバージョン切り替えは考慮してない。
    • もし切り替えたい場合は core の中に 各バージョンのディレクトリを切るか?

作業ディレクトリを作成

$ mkdir -p work/packages
$ cd work/packages

開発パッケージを作成

今回は例として laravel-hello-world パッケージという artisan コマンドに hello:world を追加するだけのサンプルパッケージを作ることにします。

$ mkdir laravel-hello-world
$ cd laravel-hello-world

composer init

$ composer init

全てEnterすると composer.json ファイルが生成されます。

{
    "name": "ucan-lab/laravel-hello-world",
    "authors": [
        {
            "name": "ゆうきゃん",
            "email": "35098175+ucan-lab@users.noreply.github.com"
        }
    ],
    "require": {}
}

"require": {} から以下の設定を追記します。

    "require": {
        "php": "^7.1",
        "laravel/framework": "^5.5.0"
    },
    "autoload": {
        "psr-4": {
            "UcanLab\\LaravelHelloWorld\\": "src/"
        }
    },
    "extra": {
        "laravel": {
            "providers": [
                "UcanLab\\LaravelHelloWorld\\Providers\\ConsoleServiceProvider"
            ]
        }
    }
  • 依存ライブラリ
  • 名前空間とオートロード
  • Laravel用のサービスプロバイダ

空のディレクトリと空のファイルを作成

$ mkdir -p src/{Console,Providers}
$ touch src/Console/HelloWorldCommand.php
$ touch src/Providers/ConsoleServiceProvider.php

src/Console/HelloWorldCommand.php 作成

<?php

namespace UcanLab\LaravelHelloWorld\Console;

use Illuminate\Console\Command;

class HelloWorldCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'hello:world';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Hello World.';

    /**
     * @return void
     */
    public function handle(): void
    {
        $this->info('Hello World.');
    }
}

src/Providers/ConsoleServiceProvider.php

<?php

namespace UcanLab\LaravelHelloWorld\Providers;

use Illuminate\Support\ServiceProvider;
use UcanLab\LaravelHelloWorld\Console\HelloWorldCommand;

class ConsoleServiceProvider extends ServiceProvider
{
    protected $defer = true;

    public function boot()
    {
        $this->registerCommands();
    }

    public function register()
    {
        // register bindings
    }

    protected function registerCommands()
    {
        $this->app->singleton('command.ucan-lab.hello.world', function () {
            return new HelloWorldCommand();
        });

        $this->commands([
            'command.ucan-lab.hello.world',
        ]);
    }

    public function provides()
    {
        return [
            'command.ucan-lab.hello.world',
        ];
    }
}

Docker環境の構築

以前の記事で紹介した構成をカスタマイズして使います。

$ cd work
$ git clone git@github.com:ucan-lab/docker-laravel5.git docker
$ cd docker

work ディレクトリでgit cloneを実行します。

work/docker/.env 修正(変更箇所のみ)

COMPOSE_PROJECT_NAME=laravel-hello-world
PROJECT_PATH=../core
PACKAGES_PATH=../packages

work/docker/docker-compose.yml 修正(変更箇所のみ)

services:
  app:
    volumes:
      - ${PACKAGES_PATH}:/packages

- ${PACKAGES_PATH}:/packages の行を追記します。

Dockerビルド&Laravelインストール

$ docker-compose up -d --build
$ docker-compose exec app composer create-project --prefer-dist "laravel/laravel=5.8.*" .

core ディレクトリ内にLaravelがインストールされます。

開発パッケージのインストール

work/core/composer.json

下記のリポジトリ設定を追記します。 どこでもいいんですけど、 require の手前あたりが綺麗な気がします。

    "repositories": [
        {
            "type": "path",
            "url": "/packages/laravel-hello-world",
            "symlink": true
        }
    ],

"symlink": true をしておくと core/vender/laravel-hello-world シンボリックリンクを貼ってくれるので、パッケージのソースコードを変更しても composer update 不要でリアルタイムで反映されます。
url は dockerから見たファイルパスを設定します。

$ docker-compose exec app composer require ucan-lab/laravel-hello-world:dev-master

動作確認

$ docker-compose exec app php artisan hello:world
Hello World.

Hello World. と表示される。

スクリーンショット 2019-08-28 20.30.25.png

さいごに

テストコードやpackagist登録は別記事にしようかなと思います。

参考

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

PhpStormプラグイン「RainbawBrackets」の紹介

RainbawBracketsとは?

カッコの対応関係を色でわかりやすくしてくれるプラグイン。
JavaScriptなどカッコが多すぎてコードが読みづらいときにとても便利。

with-material-theme-ui.png
出典: izhangzhihao/intellij-rainbow-brackets

ハイライト機能

Ctrl + 右クリックでカッコを選択すると、カッコの中がハイライトされる。

rainbow1.gif

出典: izhangzhihao/intellij-rainbow-brackets

Alt + 右クリックでカッコを選択すると、ほかのコードが暗くなり、選択したカッコの中のみがハイライトされる。

rainbow2.gif

出典: izhangzhihao/intellij-rainbow-brackets

色を変える

色を変えたい場合はSetting > Editor > Color Scheme > RainbawBracketsから変更可能

customize-colors.png

出典: izhangzhihao/intellij-rainbow-brackets

引用元

izhangzhihao/intellij-rainbow-brackets

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

PHP7.4.0ベータ3は何処に行った?

2019/07/25、PHP7.4.0ベータ1がリリースされました
2019/08/08、PHP7.4.0ベータ2がリリースされました
2019/08/22、PHP7.4.0ベータ4がリリースされました

あれ?
ベータ3は何処?

internalでもベータ3飛ばされた?と聞かれていますが、「いや、PHPのバージョンスキップは誇りであり伝統なのだよ」とか返されています。
しかしPHP7.3でも7.2でも普通にベータ3は出ているので、何のことだかよくわかりません。
ただの冗談なのか?

PHP7.4.0は2019/07/22に仕様凍結され、それ以降は以降は基本的にバグ修正だけが行われています。
以下はPHP7.4.0ベータ4で修正されたバグの一部です。

Second file_put_contents in Shutdown hangs script

シャットダウン関数内でひとつのファイルに2回file_put_contentsするとハングするというバグ。

<?php
register_shutdown_function ('shutdown');
function shutdown () {
    file_put_contents ('/tmp/test.txt', "shutdown 1 \n", FILE_APPEND | LOCK_EX);
    file_put_contents ('/tmp/test.txt', "shutdown 2 \n", FILE_APPEND | LOCK_EX); // Process exited with code 137
    var_dump(file_get_contents("/tmp/test.txt"));
    exit;
}

7.4.0α3で混入したようです。
どんな原理で起こるのかさっぱりだけど、そもそも普通はこんな使い方しないから気付かないのも仕方ない気はする。

Broken file includes with user-defined stream filters

自作ストリームフィルタを通すとinclude何故かシンタックスエラーが発生するというもの。

ベータ1とベータ2でだけ発生します。
原因はこのコミットで、自作ストリームフィルタを使うとphp_stream_statの値が正しくならないことがあるとかなんとか。
そしてどうやら原因をなおすのではなく対症療法をやったみたいなので、もしかしたら今後もまたうっかり何か起こるかもしれません。

fstat mode has unexpected value on PHP 7.4

↑に関連してなのかどうなのか、fstatがPHPバージョンによって異なる値を返してくるバグが発覚しました。

$handle = popen('echo 12', 'r');
$stats = fstat($handle);
var_dump($stats['mode']); // 7.3までは4096、7.4ベータ2では33206

ただ環境に依るようで、3v4lでは全部4480でした。つまり3v4lではバグが起こりません。

Casting a DateTime to array no longer returns its properties

DateTimeインスタンスを(array)キャストすると何も出力しないバグ。

var_dump((array) new \Datetime('2000-01-01')); // 7.3までは[date, timezone_type, timezone]、7.4.0αは[]

いやー、さすがにこれはどうなんだよ。

しかしPHP内部で使うのであればクラスのままでいいし、外に出すならJSONだけどjson_encodeは正しく動くから、実用的に問題になるような状況はあまり思いつかないです。

と思いきやSymfonyが内部的に配列を使っているらしいです。
そうなると連鎖的にLaravelが死んでPHPが終焉を迎えるので、修正しないわけにはいかないですね。

Assertion failure in openssl_random_pseudo_bytes

openssl_random_pseudo_bytesに第二引数を渡すと死ぬ。

openssl_random_pseudo_bytes(4,$b);

Segmentation faultが発生します。
なんで。

diffが1文字なんだけど、うっかりゴミ'/'が残っていただけなのだろうか。
しかもこれPHP7.1あたりからあったみたいです。

Cannot "manually" unserialize class that is final and extends an internal one

より優れたシリアライズを提供するVarExporterなるSymfonyプラグインがあります。

ところが、これがPHP7.4で__serialize/__unserializeが追加されたせいで自力でのシリアライズ/デシリアライズが不可能になったとかなんとか。
具体的にはシリアライズにReflectionClass::newInstanceWithoutConstructorを使っていて、そのヘルプには"組み込みfinalクラスはReflectionClass::newInstanceWithoutConstructorできない"と書かれているのですが、実際は組み込みクラスをextendsしたfinalクラスもReflectionClass::newInstanceWithoutConstructorできないので詰んだ(dead-end)そうです。
どういう理屈なのかいまいちよくわからないのですが、Nikitaもよくわからないとか言ってて、そしてよくわからないまま解決していました

Can't access OneDrive folder

ローカルのOneDriveフォルダにglobやscandir、file_get_contentsなどでファイルアクセスが全くできないというバグ。

かつて同じような問題があり修正されたのですが、Windows 10 May 2019 Updateによる仕様変更で再発したようです。

それにしても、たかだかファイルアクセスしたいだけなのにめちゃめちゃ大変だな。

今後の予定

2019/09/05にRC1、そして以後も2週間ごとにRCがリリースされ、正式版リリースは2019/11/28の予定です。

感想

発生すればクリティカルだけど、発生条件がややこしくて誰も気付かなかった、みたいなものが多いです。
また直近で発生したバグは、多くが別の変更に伴って発生したものです。
○○を高速化した → △△が発生した → △△を修正したら××が発生した、みたいなやつですね。

原因のひとつとしてソースコードの見通しが悪いという問題点があるのは間違いないので(あらゆるところで#ifdefの嵐だ)、もっとすっきりした書き方にした方がいいのではないかと思ったりはします。
が、しかしここは言語開発の世界です。
読みやすさなどより速度のほうが遙かに重要だ、ということなのでしょう。

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

Vagrant+VirtualBoxで作った仮想環境(CentOS)にLaravel環境構築

はじめに

以前参画していたプロジェクトにて、コンテナ内のLaravelログをCloudWatch Logsに出力したいという要望があった。
参画しておきながら、あまり開発環境構成などの理解が浅く、この機会にLinux環境にLaravel環境を構築する手順を身につける。

目次

  • Vagrant、VirtualBoxのインストール
  • 仮想環境(CentOS)の構築
  • PHPのインストール
  • composerのインストール
  • Laravelのインストール

Vagrant、VirtualBoxのインストール

仮想環境(CentOS)の構築

コマンドプロンプトを起動し、適当なフォルダへ移動します。

> d:
> cd d:\work

CentOSのイメージをダウンロードします。(これ、結構かかります)

> vagrant box add centos/7

CentOS起動用のVagrantfileの雛形を作成します。

> vagrant init centos/7

作成したVagrantfileの雛形をカスタマイズします。
D:\work フォルダに Vagrantfileファイルが出来ているので、テキストエディタで開いて、以下の編集を行います。
以下の行のコメントアウトを解除するだけです。

Vagrantfile
35|  config.vm.network "private_network", ip: "192.168.33.10"
  • 35行目の編集は、後ほどCentOS上で起動されたLaravelのビルドインWebサーバーへ接続する為に必要になります。

さあ、Vagrantを起動しましょう。

> vagrant up

カレントフォルダのvagrantfileを参照して、そこに記載してある仮想環境を構築、起動します。

仮想環境(CentOS)が起動しているので、仮想環境にSSHで接続しましょう。

> vagrant ssh

PHPのインストール

最新のLaravel 5.8 では、PHP 7.1.3 以上が必要なので、ここでは PHP 7.2をインストールすることにします。
通常なら、yum install でphpを指定すればよいのですが、既定のリポジトリには PHP7.2系が配布されていない為、リポジトリを追加する必要があります。

今回はPHP7.2を配布しているRemiリポジトリを追加しますが、Remiリポジトリを追加する為に、EPELリポジトリを追加する必要があります。
このあたりの手順は、RemiさんのWebサイトの設定ウィザードで対象OSなどを選択して、確認するのが一番かと思います。

以下で行う yum install コマンドには全て -y オプションをつけていますが、これは途中の確認を全てYESで答えるオプションです。

では、EPELリポジトリの追加から行っていきます。

$ sudo yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm

Remiリポジトリを追加します。

$ sudo yum -y install https://rpms.remirepo.net/enterprise/remi-release-7.rpm

yum-config-managerコマンド用にyum-utilsをインストールします。

$ sudo yum -y install yum-utils

remiリポジトリの有効化を行います。

$ sudo yum-config-manager --enable remi-php72

PHP 7.2 と関連パッケージをインストールします。

$ sudo yum -y install php php-devel php-mbstring php-pdo php-gd php-xml php-mcrypt php-zip php-unzip

インストールを確認します。

$ rpm -qa | grep php
$ php -v
PHP 7.2.21 (cli) (built: Jul 30 2019 14:46:08) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies

無事 PHP 7.2.21 がインストールできました。

余談(Software Collections(SCL)リポジトリ)

こちらの記事によると、PHP 7.xを配布する公式のリポジトリ Software Collections(SCL) もあるそうですが、これで配布されたPHPを利用するには、ログイン毎、ソフトウェア毎に'scl enable'コマンドでSCLを有効化する必要があり、後々面倒になりそうなので、今回の手順ではRemiリポジトリからインストールしました。

PHPの設定

PHPの設定ファイル、/etc/php.ini をviで編集します。

$ sudo vi /etc/php.ini

編集箇所は以下の通りです。

php.ini
date.timezone = "Asia/Tokyo"
mbstring.language = Japanese
mbstring.internal_encoding = UTF-8
mbstring.http_input = UTF-8
mbstring.http_output = pass
mbstring.encoding_translation = On
mbstring.detect_order = auto
mbstring.substitute_character = none 

composerをインストール

composerは、PHPのライブラリ管理ツールです。
今回はLaravelのインストールに使いますが、このLaravelのインストール時に必要なPHPライブラリを自動でインストールしてくれます。

参考:PHP開発でComposerを使わないなんてありえない!基礎編

PHPのインストールほど面倒ではないです。

# curl -sS https://getcomposer.org/installer | php

curlでダウンロードしてきたインストーラをPHPで実行しています。

ダウンロードとインストールが完了すると、カレントディレクトリにcomposer.pharというファイルができます。
これが実行ファイルなので、パスの通ったディレクトリに移動します。

$ mv composer.phar /usr/local/bin/composer

これで、composerコマンドが使えます。
早速バージョン確認してみましょう。

$ composer -v
   ______
  / ____/___  ____ ___  ____  ____  ________  _____
 / /   / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/
/ /___/ /_/ / / / / / / /_/ / /_/ (__  )  __/ /
\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
                    /_/
Composer version 1.9.0 2019-08-02 20:55:32

インストール完了です。

Laravelのインストール

composerを使ってインストールします。

$ composer global require "laravel/installer"

今回は /var/www 配下にプロジェクトを作成していこうと思います。
Laravelの新規プロジェクトを作成する際のコマンドcomposerは、一般ユーザー権限で実行されますが、/var/wwwディレクトリの書き込み権限が無いので、このディレクトリの所有者変更と書き込み権限付与を行います。

$ ls -al /var/www
total 0
drwxr-xr-x.  4 root root  33 Aug 28 09:31 .
drwxr-xr-x. 19 root root 265 Aug 28 09:31 ..
drwxr-xr-x.  2 root root   6 Jul 29 17:19 cgi-bin
drwxr-xr-x.  2 root root   6 Jul 29 17:19 html
$ cd /var/www
$ sudo chown -Rv root:$USER .
changed ownership of ‘./cgi-bin’ from root:root to root:vagrant
changed ownership of ‘./html’ from root:root to root:vagrant
changed ownership of ‘.’ from root:root to root:vagrant
$ sudo chmod -Rv g+rw .
mode of ‘.’ changed from 0755 (rwxr-xr-x) to 0775 (rwxrwxr-x)
mode of ‘./cgi-bin’ changed from 0755 (rwxr-xr-x) to 0775 (rwxrwxr-x)
mode of ‘./html’ changed from 0755 (rwxr-xr-x) to 0775 (rwxrwxr-x)

では、新しいLaravelプロジェクトを作成します。

$ composer create-project --prefer-dist laravel/laravel blog "5.6.*"

※プロジェクトが作成されると、最後にキーが同時に作成されるので、残しておきましょう。

動作確認します。

$ cd blog
$ php artisan --version
Laravel Framework 5.6.39

Laravelのバージョンが表示されればプロジェクトの作成は無事完了です。

あとは、PHPのビルドインWebサーバーを使用して動作確認するだけですが、CentOSにはSELinuxが導入されており、これが動作確認の邪魔になります。
一時的にアクセス制御を無効化して、動作確認をします。(恒久的に無効化する方法もありますが、ここでは割愛します)

$ sudo setenforce 0
$ php -S 0.0.0.0:8000 -t public
PHP 7.2.21 Development Server started at Wed Aug 28 11:04:35 2019
Listening on http://0.0.0.0:8000
Document root is /var/www/blog/public
Press Ctrl-C to quit.

この状態で、ホストOSのブラウザで、http://192.168.33.10:8000 にアクセスしてください。
laravel_success.png

おわりに

次は、Laravelの環境をBuildしてコンテナ化する手順を身につけたい

参考

https://qiita.com/inexp_eng4432/items/ec818f151b8ad516988b
https://qiita.com/ozawan/items/caf6e7ddec7c6b31f01e
https://qiita.com/takeshi_nozawa/items/ee01981b4fc338209437
https://rpms.remirepo.net/wizard/
https://eng-entrance.com/linux-selinux

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

【SwiftMailer】件名の文字化けを調査してみた話

目次

はじめに

  • PHPのSwiftMailerライブラリを使って送信したメールの件名の一部が文字化けする現象を、素人なりに調査してみた話です
  • 他の人が書いたソースを利用しているため処理内容はざっくりとしか把握できていません
  • エンコードはShiftJis
  • メーラーはThunderbird

したいこと

ほげほげシステムふがふが機能ぴよぴよ確認結果(NG)
という件名にしたいが、受信したメールでは
ほげほげシステムふがふが機能ぴよぴよ灰hネク惠比x?
になっている

「ぴよぴよ」の後ろから文字化けしているためこれを直したい

※ここでの件名は例なので適当です

やったこと

とりあえずソースを確認

文字コードを指定してるところを確認
Swift::init(function () {
    Swift_DependencyContainer::getInstance()
        ->register('mime.qpheaderencoder')
        ->asAliasOf('mime.base64headerencoder');

    Swift_Preferences::getInstance()->setCharset('shift_jis');
});

なるほどShiftJisでメール送信してるっぽい。まぁそりゃそうか。

メール送信について調べているとiso-2022-jpで送信するのが主流のようでしたがここではshift_jisで貫きます。

この時点ではSwiftMailerというものを知らないのでSwift~と書かれているものが何なのかはわかっておらず、雰囲気で読んでいました。

文字化けした部分の文字コードを調べる

予想

ShiftJisでは「確」は「8A6D」だが何故か別のエンコードになってしまっている。
例えばutf8の「8A6D」は「灰」なのかもしれない。

やってみる

文字化けしてない方(確認結果)と、文字化けした方(灰hネク惠比x?)の文字コードを書き出してみた。

JIS SJIS EUC UTF-8 UTF-16
334E 8A6D B3CE E7A2BA 78BA
4727 9446 C7A7 E8AA8D 8A8D
376B 8C8B B7EB E7B590 7D50
324C 89CA B2CC E69E9C 679C
JIS SJIS EUC UTF-8 UTF-16
3325 8A44 B3A5 E781B0 7070
68 68 68 68 0068 h
48 C8 8EC8 EFBE88 FF88
38 B8 8EB8 EFBDB8 FF78
582A 9CA8 D8AA E683A0 60E0
16 16 16 16 16
4866 94E4 C8E6 E6AF94 6BD4
78 78 78 78 x
16 16 16 16 16
?

文字コード間違っていたらすみません

結果

関係性が見えてこなかったので文字コードが変わってしまってるとかではなさそう。
まぁ件名の途中から文字コードが変わって文字化けするなんて現象が実際にあり得るのかはわかりませんが...

メールのソースを読む

なんで最初に気づかなかったのか謎ですが、ソース見れば何か書いてあるんじゃねと思ったので見てみる

やってみる
1. ソースを見たいメールを開く
2. 右上のボタン群から「その他」>「ソースを表示(V)」を選択
2019-08-21-13-51-16.png
3. 別ウィンドウでソースが開くので「Subject:」という文言を探してその後ろを参照する

「From:」や「To:」の後ろを見てみると普通にメールアドレスや差出人などが書かれている一方で、「Subject:」の後ろにはハッシュ値のような言葉が並び、=?shift_jis?という言葉が含まれていました。Subjectは件名のことです。

何がなんだかさっぱりでしたがなんとなく関係ありそうな=?shift_jis?で検索してみたら日本語の件名をShiftJisでエンコードしたってことみたい?

ネットに転がってたデコードツールで「Subject:」の後ろをコピペしたものをデコードしてみたら文字化けした件名(~ぴよぴよ灰hネク惠比x?)が出てきたので多分そういうことかな。

結果

関係がありそうなところは見つからなかった

初心に戻ってみる

冒頭に載せたソースを再掲
Swift::init(function () {
    Swift_DependencyContainer::getInstance()
        ->register('mime.qpheaderencoder')
        ->asAliasOf('mime.base64headerencoder');

    Swift_Preferences::getInstance()->setCharset('shift_jis');
});

文字コードのことは一旦忘れてソースを理解してみようと思い、Swiftなんたらを調べることに。

やってみる

普通にSwiftって打つとiOSアプリを作るのに役立ちそうな情報が出てきそうだったので「Swift php メール」とかで検索してみると、Swift Mailerというライブラリがあるらしい

なるほどそれならと「Swift Mailer Subject 文字化け」で調べてみたところそれっぽい記事を発見(symfony1.4の「SwiftMailer」を日本語対応(iso-2022-jp)してみた)

どうも、Subjectが長いと文字化けするみたい。
ググって見ると、どうも前のバージョンのSwift Mailerも長い日本語は文字化けするという情報が。

とのこと。まじか。

ということで件名をいじいじして実験してみる

これが こうなる 結果
ほげほげシステムふがふが機能ぴよぴよ確認結果(NG) ほげほげシステムふがふが機能ぴよぴよ灰hネク惠比x? ×
ぴよぴよ確認結果(NG) ぴよぴよ確認結果(NG)
あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほ あいうえおかきくけこさしすせそたちつH,h,�,�,ィ,ク,ネ,リ--8-h ×
アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤm}狛ュスハmミ ×
aaaaabbbbbcccccdddddeeeee aaaaabbbbbcccccdddH(H(X(X(X(X(X( ×
aaaaabbbbbccc...zzzzz aaaaabbbbbccc...zzzzz

結果

  • 全角日本語
    • 18文字目まで正常に表示
  • 半角日本語
    • 36文字目まで正常に表示
  • 全角英字
    • 18文字目まで正常に表示
  • 半角英字
    • 36バイトを超えても正常に表示(130文字まで確認)

結論

下記のどれかで対応できそう

  • 件名を短くする
  • 件名を半角英字にする
  • SwiftMailer以外の方法でメール送信を行う

もし「こうすれば長めの日本語件名にできるよ!」とか「ここ間違ってない?」とかあれば教えてください!

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

LAMP環境でOSS mediawiki を動かしてみる

環境確認

#  OS
$ cat /proc/version
Linux version 4.4.0-75-generic (buildd@lgw01-21) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.4) ) #96-Ubuntu SMP Thu Apr 20 09:56:33 UTC 2017
#  mysql
$ mysql --version
mysql  Ver 14.14 Distrib 5.7.27, for Linux (x86_64) using  EditLine wrapper
#  php
$ php -v
PHP 7.0.33-0ubuntu0.16.04.6 (cli) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.0.0, Copyright (c) 1998-2017 Zend Technologies
    with Zend OPcache v7.0.33-0ubuntu0.16.04.6, Copyright (c) 1999-2017, by Zend Technologies

インストール

公式サイトインストール方法を参照

https://www.mediawiki.org/wiki/Manual:Running_MediaWiki_on_Debian_or_Ubuntu/ja

# 結構処理長い
$ sudo apt-get update && sudo apt-get upgrade

MediaWiki のインストール

$ cd /tmp/
$ wget https://releases.wikimedia.org/mediawiki/1.33/mediawiki-1.33.0.tar.gz
$ tar -xvzf /tmp/mediawiki-*.tar.gz
$ sudo mkdir /var/lib/mediawiki
$ sudo mv mediawiki-*/* /var/lib/mediawiki

Mysql新規ユーザー登録

# create a NEW mysql user (new_mysql_user):
$ sudo mysql -u root -p
# Mysqlのrootパスワードを入力
Enter password: 

mysql>  CREATE USER 'new_mysql_user'@'localhost' IDENTIFIED BY 'THISpasswordSHOULDbeCHANGED';
Query OK, 0 rows affected (0.00 sec)
quit;
sudo mysql -u root -p
# Mysqlのパスワードを入力
Enter password: 

mysql> CREATE DATABASE my_wiki;
Query OK, 1 row affected (0.00 sec)

mysql> use my_wiki;
Database changed
mysql> GRANT ALL ON my_wiki.* TO 'new_mysql_user'@'localhost';
Query OK, 0 rows affected (0.00 sec)
mysql> quit;
Bye

シンボリックリンクを作成

$ sudo ln -s /var/lib/mediawiki /var/www/html/mediawiki

apache再起動

$ sudo phpenmod mbstring
$ sudo phpenmod xml
$ sudo systemctl restart apache2.service

ここまで終わったらブラウザで対象ページ開いて手順通りにインストールを実行

http://****/mediawiki/

インストールが完了したらLocalSetting.phpが入手できるので対象ディレクトリに設置

$ sudo mv /var/www/LocalSettings.php /var/lib/mediawiki/

インストール完了

http://****/mediawiki/
再度ブラウザで動作をチェックする

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

Laravelで.env が読み込めなくなった

ほんとにメモ程度に。

他の人がenvファイルに更新をしたので
そのまま加筆したら反映されなかったので

qiita.rb
php artisan config:cache
php artisan view:clear

を実行したところenvファイル自体読み込めなくなり
Undefined index:エラーに、、、

結局

bootstrap/cache/config.php
を削除したら正常に読み込めた。

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

[正規表現]かっことその中身を全てを除去

'/\([^\)]+\)/si'

$output = preg_replace('/\([^\)]+\)/si', '', mb_convert_kana($input, 'a'));

かっこを全角→半角変換した後に、
前かっこ+かっこの中身(後ろかっこを覗く)+後ろかっこ という正規表現。

/ : 正規表現の始まり
\: エスケープ記号
( : 半角前かっこにマッチ
[^\)] : これは)以外にマッチする1文字という意味。[]の中にマッチの条件を書く。^は否定。
+ : その前に出て来たやつが1回以上繰り返し。今は)以外が1文字以上あればOK。
\ : エスケープ記号
) : 半角後ろかっこにマッチ
/ : 正規表現の終わり
s : 改行も含んでマッチ
i : 大文字小文字、どちらもマッチ
(siは要らないかも?)

補足
[] : []内の1文字
[^a] : a以外の1文字

参考サイト 正規表現のパターン
参考サイト siとか。正規表現の修飾パターン

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

php 前置演算と後置演算の違い

前置演算と後置演算の違い

前置演算

test.php
$t = 0;
$s = ++$t;
print $s;

/*結果
$t 1;
$s 1;
*/
  • $s = ++$x ような形で、先に演算してから値を代入する
  • $sに代入される値は、演算後の値である
  • 前置演算は加算子以外にも、--$xのような減算子の書き方が可能
  • ++をインクリメント、--をデクリメントと呼ぶ

後置演算

test.php
$t = 0;
$s = $t++;
print $s;

/*結果
$t 1;
$s 0;
*/
  • $s = $x++のような形で、$sに代入してから演算する
  • $sに代入される値は、演算前の値である
  • 後置演算は加算子以外にも、$x--のような減算子の書き方が可能
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

これだけ知っておけばとりあえずconcrete5でサイトを作れるまとめ

Web制作者にとって、新しいCMSに手を出すときに気になる要素は、テンプレートの作り方、デザインカスタマイズの自由度だと思います。特にいったんクライアントがデザインにGOサインを出したら、実装の都合で変更しにくい風潮があると感じられる日本では特に…:sweat_smile:

デザインカスタマイズについて、いかに最低限のドキュメントだけ読んで何とかなるかという点では、concrete5はCMSの中でもピカイチだと思います。そこで、個人的に「これだけ知っていれば普通のサイトは作れるようになる」と思う知識をまとめてみました。ここでいう普通のサイトとは、concrete5の標準機能でできる範囲のサイトという意味です。

なぜconcrete5は、ドキュメントを読む量が少なくて済むのでしょうか。それは、サイトを作り上げるのに、マウス操作で実現する範囲が他のCMSより多いからだと思います。逆にいうと、テンプレートは少ない知識で楽に作れますが、反対にCMSの操作に慣れる時間は相応に必要になりますで、そこはトレードオフとしてご理解いただければと思います。

concrete5の操作については、YouTubeの勉強会動画シリーズもご参照ください。

コンテンツの概念

必ず頭に入れておきたいのは、concrete5にとってのコンテンツの概念です。

利用者の多いWordPressでは、投稿、固定ページ、アーカイブページ、のようなページごとの分類があり、それぞれにあらかじめ機能が異なります。concrete5では、そのような機能に依存したページの分類はありません。全てのWebページは、等しくただのページです

concrete5では、ページはブロックを配置するための器に過ぎません。WYSIWYGエディタでテキストが編集できる記事ブロックを置けば固定ページになりますし、お知らせの一覧を表示するためにページリストブロックを置けばアーカイブページになりますし、サイト内検索結果を表示するために検索ブロックを置けば検索結果ページになります。

置くブロックによってページの性格が変わるというconcrete5の特徴により、サイトマップ設計の自由度が増します。WordPressではサイトマップ設計に合わせてパーマリンクをいじるのが一苦労ですが、そう言った苦労はありません。逆に、適切なブロックを置くまではページは空っぽ。この自由度が逆に、「何をしていいか分からない」という最初のつまづきを招きます。まずは、いろんなブロックを置いていじってみて、ページとブロックの関係性を感覚でつかみましょう。

ブロックを置ける場所はエリアで規定されています。多くのテーマは、全幅、右サイドバー、左サイドバー、のようなレイアウトのバリエーションをページテンプレートとして持っており、ページテンプレートでエリアの配置があらかじめ決まっています。

エリアは、画面上の操作でレイアウトを内包することができ、レイアウトのパターンもデザイナーの自由に定義して増やすことができます。

以上の構成を図示したものが下図になります。

Untitled.png

図を見ながら、concrete5の操作動画も見比べてみてください。

デザインカスタマイズのために、何を作るべきか

concrete5では、テーマとブロックテンプレート、この2つを作る作業がデザインカスタマイズになります。

オーバーライドの概念

実際にデザインカスタマイズに着手する前に理解しておくべきなのがオーバーライドの概念です。と言っても、難しいものではありません。

concrete5をダウンロードすると、applicationフォルダとconcreteフォルダ、packagesフォルダに大きく分かれています。

packagesフォルダは、マーケットプレイスから入手したテーマやアドオンが格納される場所ということだけ覚えておいてください。

concrete5のコアファイルはconcreteフォルダに格納されています。この中身は変更不可です。なぜなら、concrete5をバージョンアップすると、concreteフォルダの中身が上書きされるので、自分で何か中身を変更していたとしても、それが消えてしまうからです。concrete5をカスタマイズしたければ、applicationフォルダを使います。

applicationフォルダ内は、concreteフォルダとほとんど似たような構成のフォルダが並んでいますが、中身は空っぽです。concreteフォルダ内のカスタマイズしたいファイルを、applicationフォルダにコピーしてから変更する。これがオーバーライドの原則です。

例えば、concrete5に同梱されているjQueryのバージョンが古いので、新しくしたいなと思ったとします。ファイルは以下に格納されています。

/concrete/js/jquery.js

このファイルをapplicationフォルダでオーバーライドできます。新しいバージョンのjQueryファイルを、以下に設置してみてください。

/application/js/jquery.js

設置するだけで、concrete5はオーバーライドされたjquery.jsを読み込んでくれます。簡単ですね!

実際には、リクエストのたびにオーバーライドの有無を確認しているとサーバー負荷がかかりますので、どのファイルがオーバーライドされているかはキャッシュされています。オーバーライドしたのに反映されないぞ、と思った場合は、管理画面からキャッシュをクリアしてみましょう。

テーマ

テーマとはすなわち、ページテンプレートとCSSやJavaScriptなどのアセットの集合体です。

テーマのファイル構成

concrete5に同梱されているElementalテーマは、下記のようなファイルで構成されています。

ファイル名 用途
css テーマで使用するcssファイルの置き場所です。フォルダ名に特に決まりはありません。
elements ヘッダーやフッターなど、どのレイアウトでも共通にインクルードするパーツの置き場所です。フォルダ名に特に決まりはありません。
default.php デフォルトページテンプレートです。テーマの基本となるレイアウトです。ファイル名は default.php である必要があります。
blank.php, full.php, left_sidebar.php, right_sidebar.php ページテンプレートです。テーマで用意されているレイアウトのバリエーションの数だけ存在します。ファイル名は自由につけて構いません。
page_forbidden.php, page_not_found.php 403, 404エラー用のテンプレートです。
page_theme.php テーマの設定ファイルです。レイアウトの定義などを行います。
view.php シングルページ用のテンプレートファイルです。ログイン画面やマイアカウントページなどを自作テーマで表示したい場合以外は、なくても良いです。

ページテンプレートは、管理画面から登録することで、使えるようになります。登録の際のハンドル名とファイル名が対応します。

Screen Shot 2019-08-28 at 8.39.55.png

登録したページテンプレートは、各ページのデザインメニューで切り替えに使用できます。

Screen Shot 2019-08-28 at 8.42.50.png

テーマの置き場所

自作テーマはapplicationフォルダ内に設置します。

/application/themes/my_great_theme

my_great_theme 部分は、テーマのハンドル名として扱われます。英小文字とアンダーバーの組み合わせであれば、自由に設定できます。

page_theme.php の作成

テーマの名前や、レイアウトの定義などは page_theme.php に記述します。

/application/themes/my_great_theme/page_theme.php

中身の基本は下記のようになります。

自作テーマのpage_theme.php作例
<?php
namespace Application\Theme\MyGreatTheme;

use Concrete\Core\Page\Theme\Theme;

class PageTheme extends Theme
{
    /**
     * テーマ名
     * /
    public function getThemeName()
    {
        return '俺のスゲーテーマ';
    }

    /**
     * テーマの説明文
     * /
    public function getThemeDescription()
    {
        return '俺による俺のための俺カッケーなテーマ';
    }
}

冒頭の namespace で始まる宣言が名前空間です。この MyGreatTheme が、テーマのハンドルの my_great_theme の各単語の先頭を大文字にして、アンダーバーを詰めたものになります。ここのスペルミスでテーマが認識されないのが地味にハマりポイントなのでご注意を。

あとは色々な設定がこのファイルからできるのですが、ドキュメントを見たり、Elementalのpage_theme.phpからコピペで大体なんとかなります。

ページテンプレートの作成

これもぶっちゃけ Elemental テーマからのコピペでなんとかなりますが、やるべきことが何かはおさらいしておきます。やることはほぼこの3点のみ。

concrete5のUIを読み込む

concrete5のツールバーや、各種メニューを読み込ませるために入れる必要があるコードです。

<!-- head タグ内にコピペ -->
<?php View::element('header_required'); ?>
<!-- body の開始タグ直後にコピペ -->
<div class="<?php echo $c->getPageWrapperClass()?>">
<!-- body の閉じタグ直前にコピペ -->
</div>
<?php View::element('footer_required'); ?>

テーマのCSSやJSのパスを通す

ぶっちゃけ /application からパスを書いても動きますが、一応PHPでテーマのパスを出力するのがお作法です。

<link rel="stylesheet" type="text/css"
 href="<?php echo $view->getThemePath()?>/css/style.css">

エリアを定義する

ブロック配置可能にしたい要素の中身に、エリアの定義を記述します。これも非常に簡単でコピペでOKです。

エリアの定義
<nav>
<?php
/**
 * グローバルエリア
 * ヘッダーのロゴやフッターのコピーライドなどどのページでも共通の領域
 * /
$a = new GlobalArea('Header Navigation');
$a->display();
?>
</nav>

<main>
<?php
/**
 * 通常のエリア
 * ページごとに中身が異なります
 * /
$a = new Area('Main');
$a->display($c);
</main>

エリア名は自由につけて構いません。日本語でも動きますが、慣習としてエリア名は英語で記述し、必ず1画面に1つ Main エリアを設置します。

このエリアの中にブロックが配置できるわけですが、さらにレスポンシブグリッドでの分割をサポートしたい場合は、ちょこっとやることがあります。詳しくはヘルプを参照。

以上の3点でテーマ作成作業は終了なのですが、他にもいくつか覚えておくと良い記述があります。

アクセス拒否

ページテンプレートのPHPはサーバー上にアップされていますので、直接 /concrete/themes/elemental/default.php などのURLでアクセス可能です。この時、不要なエラーが出力されてサーバー情報が露出することを防ぐため、全てのテンプレートファイルの冒頭に、直接アクセスされた時に「アクセス拒否」と表示されるコードを入れるのがお作法です。

アクセス拒否
<?php defined('C5_EXECUTE') or die("Access Denied.");

ファイルのインクルード

ヘッダーやフッターなど、どのページテンプレートでも共通のパーツをインクルードしたいときは、次のように書きます。

ファイルのインクルード
<?php $this->inc('elements/header.php'); ?>

lang属性値の出力

多言語サイトを構築している際は、lang属性が正しい言語で出力されるようにしておきましょう。

lang属性値の出力
<html lang="<?php echo Localization::activeLanguage() ?>">

ブロックテンプレート

テーマができたら、次なブロックテンプレートです。concrete5では、ページに設置したブロックは、個別に画面上でテンプレートを変更できます。次のスクリーンショットは、オートナビブロックのデザイン&カスタムテンプレートメニューから、テンプレートを画面上で変更しようとしているところです。

Screen Shot 2019-08-28 at 9.35.28.png

画面を見ると、「(何も選択されていません)」「パンくず」「レスポンシブヘッダーナビゲーション」の3つの選択肢があるのが分かると思います。これら3つのテンプレートは、下記のディレクトリに格納されています。

/concrete/blocks/autonav/view.php
/concrete/blocks/autonav/templates/breadcrumb.php
/concrete/blocks/autonav/templates/responsive_header_navigation/view.php

concrete5のブロック関連のファイルは、blocksディレクトリに格納されています。その中に、オートナビブロックであればautonav、ページリストブロックであればpage_listなど、それぞれディレクトリが分かれています。

ブロックのディレクトリ内のview.phpがデフォルトのテンプレートになり、画面で「(何も選択されていません)」を選択するとこのファイルが使われます。ブロックのディレクトリ内のtemplatesディレクトリ内に、カスタムテンプレートを設置できるという仕組みになっています。

それでは、concrete5に同梱されている「パンくず」テンプレートをカスタマイズしてみましょう。オーバーライドの原則を思い出してください。

/concrete/blocks/autonav/templates/breadcrumb.php

このファイルを複製して、

/application/blocks/autonav/templates/breadcrumb.php

ここに置く。シンプルですね。あとは、コーディングに合わせて <ol class="breadcrumb"><ul class="breadcrumb_nav"> に変更するなり、好きに改変すればOK。

既存のテンプレートの改変ではなく、追加したいときはどうすれば良いか?単にファイルを設置すればOKです。

/application/blocks/autonav/templates/my_wonderful_navigation.php

それぞれのブロックテンプレートは、素のPHPです。独自のテンプレートエンジンは採用されていません。シンプルに、それぞれのブロックの view.php をapplicationフォルダに複製して、必要なclass名の変更やdivの追加などやっていけば大丈夫です。

「え?それだけ?もっと解説ないの?」と思われるかもしれませんが、ぶっちゃけ、ないです。なぜかというと、ナビゲーションでサイト内のどんなページを何階層、何順で表示するかとか、新着情報の一覧をどこに何件表示するかとか、WordPressであればテンプレートに書いて定義することは、全部画面上で、マウス操作で設定するからです。

そのため、concrete5ではテーマもテンプレートも慣れればあっという間にできあがります。どちらかというと、できたテンプレートを画面上で設定していくほうが時間がかかるくらいです。ここ、ブログ型CMSをずっと触ってきた人にとっては、大きな発想の転換が必要です。

ブロックテンプレートは、PHPとCSS、JavaScriptをセットで作ることもできます。

その他の知識

テーマとブロックテンプレートの作り方が分かれば、concrete5でサイトを作れます。祝・卒業!頑張ってください!

と言いたいところですが、その他必須で知っておくべき知識もいくつか補足しておきます。

ブロックテンプレートは、コアのview.phpを複製して改変すれば良い、とお伝えしましたが、その中で見慣れないconcrete5独自のPHPがありますので、紹介しておきます。

翻訳関数

concrete5のテンプレートに度々登場する t() 関数は、中の英語を翻訳して表示するための関数です。日本語のみのサイトを作るなら使う必要がないくらいです。多言語サイトであれば、この関数で括ったテキストは、管理画面の「サイトインターフェースを翻訳」ページから翻訳を登録できます。

t()関数の使用例
<?=t('Empty Auto-Nav Block.')?>

ヘルパー

テンプレート内では、文字列や画像、日付の処理を助けるヘルパークラスがよく使われています。

ヘルパーの使用例
<?php
// テキストヘルパーの呼び出し
$th = Core::make('helper/text');
// テキストヘルパーを使って指定した文字数で切り詰め
$th->wordSafeShortText($description, 50);

// 日付ヘルパーの呼び出し
$dh = Core::make('helper/date');
// 日付ヘルパーを使ってローカライズされた書式で日付を表示
$date = $dh->formatDateTime($dateTime);
?>

レシピ集

その他、デザインカスタマイズの際に必要な知識は、日本語公式サイトのレシピ集にまとめて行っています。まだまだ記載されていないTipsもありますので、デザインカスタマイズで困った!ということがある方は、コメント欄にてレシピ集への追加をリクエストください。

それでは、concrete5で良いWebデザインライフを!

あ、世界一わかりやすいconcrete5導入とサイト制作の教科書も見てね!

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

phpで自然順ソートをする

仕事で画面に表示するデータの表示順がおかしいと報告を受け調査したところ、
下記のような感じの処理が問題だったと判明。

// 実際には外部から取得してきたデータの配列
// 末尾がシーケンス番号になっている文字列がバラバラの順番で配列に入ってる
$datas = [
    'aaa_7',
    'aaa_1',
    'aaa_8',
    'aaa_5',
    'aaa_2',
    'aaa_6',
    'aaa_11',
    'aaa_9',
    'aaa_10',
    'aaa_3',
    'aaa_4',
];

usort($datas, function (string $a, string $b) {
    return strcmp($a, $b);
});

上記の処理で実現したかったのは下記のように「配列内の値を末尾のシーケンス順に並び替える」

array(11) {
  [0]=>
  string(9) "aaa_1"
  [1]=>
  string(9) "aaa_2"
  [2]=>
  string(9) "aaa_3"
  [3]=>
  string(9) "aaa_4"
  [4]=>
  string(9) "aaa_5"
  [5]=>
  string(9) "aaa_6"
  [6]=>
  string(9) "aaa_7"
  [7]=>
  string(9) "aaa_8"
  [8]=>
  string(9) "aaa_9"
  [9]=>
  string(10) "aaa_10"
  [10]=>
  string(10) "aaa_11"
}

だったのですが実際には下記のような結果に

array(11) {
  [0]=>
  string(9) "aaa_1"
  [1]=>
  string(10) "aaa_10"
  [2]=>
  string(10) "aaa_11"
  [3]=>
  string(9) "aaa_2"
  [4]=>
  string(9) "aaa_3"
  [5]=>
  string(9) "aaa_4"
  [6]=>
  string(9) "aaa_5"
  [7]=>
  string(9) "aaa_6"
  [8]=>
  string(9) "aaa_7"
  [9]=>
  string(9) "aaa_8"
  [10]=>
  string(9) "aaa_9"
}

早速ですが上記のコードをどのように修正したかの結果だけ先に書いておきます

usort($datas, function (string $a, string $b) {
    // strcmp()→strnatcmp()
    return strnatcmp($a, $b);
});

strcmp()strnatcmp()に置き換えただけですね。
これで先ほどの配列内のデータを末尾のシーケンス順に並び替えることができました。

ではまず元々の処理がどのようなものだったかを解説していきますが、メインの処理はたった数行なので、ほぼ使用されている関数の説明です。

usort()は配列の値をユーザー定義の関数でソートする関数です。
そしてそのusort()のコールバック関数内で使用されているstrcmp()は文字列を先頭から一文字ずつ比較していき、
同一ではないと判断された時点でその差を返す関数です。
文字列の比較はバイト列にて行い、同一ではないとわかった場合に返却する差はそのバイト列の差となっています。

上記の通り先頭から1文字ずつ比較していくため、aaa_10aaa_2の比較時に1と2で比較されてしまい、aaa_10が前でソートされてしまうという結果になりました。
そのため、比較部分の関数を自然順比較が可能なstrnatcmp()に変更しています。

また、自然順という言葉があまり聞きなれない言葉だったためそちらも調べてみたのですが、wikipediaに寄ると「アルファベット順を基本とする,複数桁の数字が単一の文字として順序付けられるような照合規則」とのこと。
複数桁の数字が単一の文字として順序づけられたため、aaa_10aaa_2の比較は10と2の比較になり、期待通りのソート結果となったということですね。

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

【cakephp3】Kintoneに保存されたユーザー情報を使用して認証を行う

やりたいこと

cakephp3でログイン機能を実装する際、認証情報として使用するユーザーデータとしてKintone上のアプリケーションに登録されたユーザー情報を使いたい。

ちなみに今回使ったのがKintoneというだけで、ユーザー情報の場所が変わってもここで説明したやり方を応用すれば対応可能です。

参考にしたページ

フレームワークのことなら公式サイトを見ればおおよそ解決!みんな知ってるね!

環境

php => 7.3.6
cakephp => 3.8.0

前提

  • ユーザー情報が登録されているアプリケーションがKintone上に存在している(項目はとりあえずユーザー名とパスワードのみあれば可。また、passwordは登録時にハッシュ化されていること)

やってみよう

というわけで実際にどうやるのかを説明していきますが、今回の場合は大まかに分けて手順は二つです。

  1. cakephpの Cake\Auth\BaseAuthenticate を継承したKintone認証用のクラスを作成する。
  2. 1で作成した認証用クラスをAuthコンポーネントの呼び出し時に認証クラスとして使用されるように設定する。

トテモカンタンネ。

Kintone認証用のクラスを作成

src/Auth/KintoneAuthenticate.php
<?php

namespace App\Auth;

use Cake\Auth\BaseAuthenticate;
use Cake\Http\Response;
use Cake\Http\ServerRequest;
use CybozuHttp\Api\KintoneApi;
use CybozuHttp\Client;

/**
 * Kintone上に登録された情報での認証を行う
 */
class KintoneAuthenticate extends BaseAuthenticate
{
    public function authenticate(ServerRequest $request, Response $response)
    {
        $fields = $this->_config['fields'];
        if (!$this->checkFields($request, $fields)) {
            return false;
        }

        return $this->_findUser(
            $request->getData($fields['username']),
            $request->getData($fields['password'])
        );
    }

    protected function checkFields(ServerRequest $request, array $fields)
    {
        foreach ([$fields['username'], $fields['password']] as $field) {
            $value = $request->getData($field);
            if (empty($value) || !is_string($value)) {
                return false;
            }
        }

        return true;
    }

    protected function _findUser($username, $password = null)
    {
        if ($password === null) {
            return false;
        }

        $client = new KintoneApi(
            new Client(
                [
                    'domain' => {ドメイン},
                    'subdomain' => {サブドメイン},
                    'use_api_token' => true,
                    'token' => {apiトークン},
                ]
            )
        );

        $query = "ユーザー名 = \"" . $username . "\"";

        $response = $client->records()->get(
            {kintone上のアプリケーションid},
            $query
        );

        if (count($response['records']) < 1) {
            return false;
        }

        $record = $response['records'][0];

        // パスワードハッシュ用のクラスを取得
        $hasher = $this->passwordHasher();
        $hashedPassword = $record['パスワード']['value'];

        // 入力されたパスワードとハッシュ化されたパスワードが一致するか確認
        if (!$hasher->check($password, $hashedPassword)) {
            return false;
        }

        $this->_needsPasswordRehash = $hasher->needsRehash($hashedPassword);

        $fields = $this->_config['fields'];

        // ユーザー情報を返す
        return [
            $fields['username'] => $username,
            $fields['password'] => $hashedPassword
        ];
    }
}

色々書いてありますが、要は Cake\Auth\BaseAuthenticate を継承した認証用クラスを作成し、 authenticate 内で実際の認証処理を書いてあげるだけです。
その際の返却値は、失敗の場合はfalse、成功した場合はユーザー情報を配列で返します。

また、このサンプルではKintoneからのデータ取得用に ochi51/cybozu-httpを使用していますので、こちらの使い方がわからない方は各自別途検索又は自分の使用している取得手段にそれぞれ置き換えしてください。
ユーザー情報取得についてはほぼライブラリで行なっているので、パスワードの比較の部分をもう少し詳細に説明します。

// パスワードハッシュ用のクラスを取得
$hasher = $this->passwordHasher();
$hashedPassword = $record['パスワード']['value'];

// 入力されたパスワードとハッシュ化されたパスワードが一致するか確認
if (!$hasher->check($password, $hashedPassword)) {
    return false;
}

上記の $this->passwordHasher() でパスワードハッシュ用のクラスを取得できます。
当然データ登録時に使用したハッシュクラスでないと正しくパスワードの比較が行えないため、気をつけてください。
今回は特に何も指定していないため、 Cake\Auth\DefaultPasswordHasher が使用されています。
その後 check() でパスワードの一致を行なっていますが、 DefaultPasswordHasher ではパスワードハッシュの処理と比較の処理は下記のように定義されています。

Cake\Auth\DefaultPasswordHasher
// ハッシュ時の処理
public function hash($password)
{
    return password_hash(
        $password,
        $this->_config['hashType'],
        $this->_config['hashOptions']
    );
}

// 比較時の処理
public function check($password, $hashedPassword)
{
    return password_verify($password, $hashedPassword);
}

内部的にはphpの組み込み関数である password_hash()password_verify() が呼ばれているだけですね。

// ユーザー情報を返す
return [
    $fields['username'] => $username,
    $fields['password'] => $hashedPassword
];

続いてログインが成功した場合に返す情報ですが、現在ログインしているユーザー情報(Contollerで $this->Auth->user() を使用した時に取れる情報)としてセッションに入るので、必要な情報は全て入れておきましょう。

Authコンポーネントの呼び出し時に認証クラスとして使用されるように設定する

これは非常に簡単。

<?php

namespace App\Controller;

class MembersController extends AppController
{
    public function initialize()
    {
        parent::initialize();
        $this->loadComponent('Auth', [
            'authenticate' => [
                'Kintone' => [
                    'fields' => [
                        'username' => 'username',
                        'password' => 'password',
                    ],
                ],
            ],
        ]);
    }
}

これだけでOKです。
$this->loadComponent() でAuthComponentを読み込む際の第二引数に、 authenticate というキー名で使用したい認証クラス名を渡してあげます( authenticate の部分は不要です。)

fields でも設定が入っていますが、ユーザー名とパスワードが何というキー名で渡ってくるかを設定しています。
今回は省いていますが、ctp側でそれぞれキー名と同じ usernamepassword という名前で渡すようにしているためキー名と値は一緒になっています。
認証クラスの作成時に $this->_config['fields'] という記述があったとおもいますが、それはこの設定を取得していたんですね。

ここまで設定したらログインフォームから値を入力し、実際にログイン処理を試してみてください。
kintone上のユーザー情報を元にログイン認証を行うことができるようになっているはずです。

余談

今回認証用クラスを作成するにあたって、既存の Cake\Auth\BaseAuthenticate 内の処理をみていたのですが、その中のユーザー取得用関数にこんな記述が

Cake\Auth\BaseAuthenticate
protected function _findUser($username, $password = null)
{
    $result = $this->_query($username)->first();

    if (empty($result)) {
        // Waste time hashing the password, to prevent
        // timing side-channels. However, don't hash
        // null passwords as authentication systems
        // like digest auth don't use passwords
        // and hashing *could* create a timing side-channel.
        if ($password !== null) {
            $hasher = $this->passwordHasher();
            $hasher->hash($password);
        }

        return false;
    }
// ...(略)

データを取得した結果が空の場合、パスワードがあればとりあえずハッシュ・・・?
コメント見る限りサイドチャネル攻撃の対応っぽいことが書いてあり、多少の推測はできるのですが細かい理屈がよくわからなかったです。

あと今回要点だけに絞って大分簡略化して説明書いちゃいましたが、もしデータ登録とか含めた全体見たいとか要望多ければそちらも書くかも。









ぼく「これで僕もAuthComponentを使用した様々な認証タイプに対応できるようになったぞ!」
cake君「4系からAuthComponent非推奨だぞ」
スクリーンショット 2019-08-28 2.56.17.png
ぼく「あっ、ふ〜ん」(絶望)
4系楽しみですね。

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

LaravelにおけるN対Nリレーションをやってみた

目的

Laravel上でN対Nリレーションのものを何か実装してみる。

前提

下記の環境で実装した。

  • PHP7.1.x
  • Laravel5.8.x
  • PostgreSQL11.4

上記環境にてN対Nのリレーションを用いたものを実装してみた。

サンプル仕様

本とその著者について考える。

本には著者が必ずいるが一人とは限らない。また著者は本を複数冊出す。

よって本と著者の関係はN対Nになる。

そこで、下記のようなテーブル構造とする。

img1.png

サンプルのために簡略化のため本はタイトルと内容のみ、著者は名前のみとする。  

画面構成は

  • 書籍一覧

    img2.png

  • 書籍詳細
    img3.png

の2画面とする。  
また簡略化のため、authorsテーブルには既に3人の著者を登録済みとする。

実装手順

下記の手順で実装した。

  • モデル作成
  • コントローラー作成
  • ビューの作成

モデル作成

まず、books(書籍データ)のモデルを作成する。Laravelなのでartisanで雛形作成する。

% php artisan make:model book --migration

以上でBook.phpとbooksのミグレーションファイルが作成される。
同様にauthors(著者データ)のモデルも作成する。

% php artisan make:model author --migration

まず、ミグレーションを片付けてしまおう。

****_create_books_table.php
<?php
 省略  

class CreateBooksTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('books', function (Blueprint $table) {
            $table->bigIncrements('id'); // ここから
            $table->string("title");
            $table->text("memo");
            $table->timestamps();        // ここまでを追加記述
        });
    }

 省略  
}
****_create_authors_table.php
<?php
 省略  

class CreateAuthorsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('authors', function (Blueprint $table) {
            $table->bigIncrements('id'); // ここから
            $table->string("name");
            $table->timestamps();        // ここまでを追加記述
        });
    }
 省略  
}

また、リンクテーブルを作成する必要があるので、モデルは作成する必要がないため、ミグレーションのみ作成する。

% php artisan make:migration author_book
****_create_author_book_table.php
<?php
 省略  
class CreateBookAuthor extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('book_author', function (Blueprint $table) {
            $table->bigIncrements('id');  // ここから
            $table->integer("book_id");
            $table->integer("author_id"); // ここまでを追加記述
        });
    }
 省略  
}

ここでまずテーブルを作成してしまう。

% php artisan migrate

これでデータベースの準備は整ったので、モデルのリレーション設定を行う。

まずbookモデル

Book.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    public function authors() {
      return $this->belongsToMany(Author::class);
    }
}

authorsとリンクするので、authors()メソッドを作成して、belongsToMany()を上記のように記述する。

同様にauthorsモデル

Book.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Author extends Model
{
    public function books() {
      return $this->belongsToMany(Book::class);
    }
}

以上で、モデルの作成は終了である。

コントローラ

booksのコントローラーを作成する。

例によってartisanにて雛形を作成する。

% php artisan make:controller BookController --resource --model=Book

以上でコントローラーの雛形が作成される。

ルーティングは下記のように設定した。

routes/web.php
Route::resource("books","BookController");

コントローラーの中身を埋めていこう。

index(書籍一覧)

indexは特に説明することはないが、Bookモデルでリレーションの設定をしているため、
Authorを取得するコードを記述する必要はない。

BookController.php
public function index()
{
    $books = Book::get();
    return view("books.index",["books"=>$books]);
}

create(書籍登録画面)

新規登録画面である。Authorのチェックボックスを作成するため、Authorのリストが必要になる。

BookController.php
public function create()
{
    $authors = Author::pluck("name","id")->toArray();
    return view("books.create",compact('authors'));
}

store(書籍登録実行)

bookレコード作成とauthorとのリンク(author_book)レコードの作成を行う。

BookController.php
public function store(Request $request)
{
    // フォーム値からauthor_idの配列を抜き出す。
   $authors = $request->input("author_id",[]);
   unset($request["author_id"]);

   // bookの登録
   $book = new Book();
   $book->title = $request->title;
   $book->memo = $request->memo;
   $book->save();

    // authorとのリレーションレコードを登録    
   $book->authors()->attach($authors);

    return redirect(
            route("books.edit",$book->id))
        ->with("flash_message","保存しました");
}
$book->authors()->attach($authors);

の一行でリンクテーブルのレコードを作成してくれる。

edit(書籍更新画面)

更新画面である。これもAuthorのチェックボックスを作成するため、Authorのリストが必要になる。

BookController.php
public function edit(Book $book)
{
   $authors = Author::pluck("name","id")->toArray();
   return view("books.edit",["book"=>$book,"authors"=>$authors]);
}

update(書籍更新実行)

bookレコード更新とauthorとのリンク(author_book)レコードの更新を行う。

BookController.php
public function update(Request $request, Book $book)
{
    // bookの更新
   $book->title = $request->title;
   $book->memo = $request->memo;
   $book->save();

   // authorとのリレーションレコードの更新 
   $book->authors()->sync($request->input('author_id',[]));

   return redirect(
            route("books.edit",$book->id))
        ->with("flash_message","保存しました");
}

これもリンクテーブルの更新は1行で済む。

$book->authors()->sync($request->input('author_id',[]));

delete(書籍削除)

bookレコード削除とauthorとのリンク(author_book)レコードの削除を行う。

BookController.php
public function destroy(Book $book)
{
    // authorとのリレーションレコードの削除
    $book->authors()->detach();

    // bookの削除
    $book->delete();

    return redirect(
            route("books.index"))
        ->with("flash_message","削除しました");
}

これもリンクテーブルの削除は1行で済む。

$book->authors()->detach();

ビューの作成

最後にビューである。

index(書籍一覧)

一覧表示部分のみ抜粋する。

<table class="table table-stripe">
  <tr>
    <th>ID</th>
    <th>タイトル</th>
    <th>著者</th>
  </tr>
  @foreach($books as $book)
  <tr>
    <td>{{ $book->id }}</td>
    <td>{{ $book->title }}</td>
    <td>@foreach($book->authors as $author) {{$author->name}} @endforeach</td>
    <td>
      <a href="{{ route('books.edit',$book->id) }}" class="btn btn-primary">更新</a>
      <form action="{{ route('books.destroy',$book->id) }}" id="form_{{ $book->id }}"
        method="post" style="display:inline">
        {{ csrf_field() }}
        {{ method_field('delete') }}
        <a href="#" data-id="{{ $book->id }}" onClick="delAdmin(this);" class="btn btn-danger">削除</a>
      </form>
    </td>
  </tr>
  @endforeach
</table>
<a href="{{ route('books.create') }}" class="btn btn-primary">追加</a>

$book->authorsにリンクされているauthorが入っているため、$author->nameを表示するだけで良い。

create(新規登録)

フォームのみ抜粋する。

  <form method="post" action="{{ route('books.store') }}" enctype="multipart/form-data">
    <input type="hidden" name="_token" value="{{ csrf_token() }}">
    <div class="form-group">
      <label for="title">タイトル</label>
      <input type="text" class="form-control" name="title" id="title">
    </div>
    <div class="form-group">
      <label for="memo">内容</label>
      <textarea class="form-control" rows="3" name="memo" id="memo"></textarea>
    </div>
    <div class="form-group">
      <label for="author">著者</label>
      @foreach( $authors as $key => $author)
        <label class="checkbox-inline">
          <input type="checkbox" name="author_id[]" value="{{ $key }}">{{ $author }}
        </label>
      @endforeach
    </div>
    <input type="submit" class="btn btn-primary" value="登録">
    <a href="{{ route('books.index') }}" class="btn btn-default">一覧へ</a>
  </form>

edit(更新画面)

フォームのみ抜粋する。

  <form method="post" action="{{ route('books.update',$book->id) }}" enctype="multipart/form-data">
    <input type="hidden" name="_method" value="PUT">
    <input type="hidden" name="_token" value="{{ csrf_token() }}">
    <div class="form-group">
      <label for="title">タイトル</label>
      <input type="text" class="form-control" name="title" id="title" value="{{ old('title',$book->title) }}">
    </div>
    <div class="form-group">
      <label for="memo">内容</label>
      <textarea class="form-control" rows="3" name="memo" id="memo">{{ old('memo',$book->memo) }}</textarea>
    </div>
    <div class="form-group">
      <label for="author">著者</label>
      @foreach( $authors as $key => $author)
      @php
        $bCheck = false;
        if ( isset($book->authors) && $book->authors->contains($key)) {
          $bCheck=true;
        }
      @endphp
        <label class="checkbox-inline">
          <input type="checkbox" name="author_id[]" value="{{ $key }}" @if ($bCheck==true) checked @endif>{{ $author }}
        </label>
      @endforeach
    </div>
    <input type="submit" class="btn btn-primary" value="登録">
    <a href="{{ route('books.index') }}" class="btn btn-default">一覧へ</a>
  </form>

下記のコードで、リンクされているかどうか判定する。

@foreach( $authors as $key => $author)
@php
  $bCheck = false;
  if ( isset($book->authors) && $book->authors->contains($key)) {
    $bCheck=true;
  }
@endphp

考察

以上のようにN対Nのリレーションが相当楽に実装できるのである。  
少し気持ちが悪いところはbookとauthorのリンクテーブルは

 book_author
としたいところであるが、うまく行かなかった。どうやらアルファベット順でないと
ダメなようである。

ソース一式はGithubにアップした。

https://github.com/kingcony/bookauthor_Sample

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