既存の Laravel プロジェクトに Closure Table を導入する
最初からツリー構造のデータを扱うことがわかっていれば、 franzose/closure-table を導入して、こちらの記事(
Laravel|Closure Tableで階層の深さが動的なカテゴリ構造を扱う - わくわくBank )などを参考にしながら設定すればよいのですが、後からコメントを階層化したいみたいなオーダーが出てきたときにどうすればよいか、というのが今回の記事のお話です。
1. 必要なライブラリをインストール
$ composer require franzose/closure-table
2. マイグレーションファイルの作成
Closure Table 用のテーブルを作成するとともに、元々のモデル用のテーブルに必要なカラムを追加します
(以下では、 comments テーブルをツリー化することを想定しています)
Closure Table 用のテーブルを作成
<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateCommentClosuresTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('comment_closures', function (Blueprint $table) { $table->increments('closure_id'); $table->integer('ancestor', false, true); $table->integer('descendant', false, true); $table->integer('depth', false, true); $table->foreign('ancestor') ->references('id') ->on('comments') ->onDelete('cascade'); $table->foreign('descendant') ->references('id') ->on('comments') ->onDelete('cascade'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('comment_closures'); } }
既存の comments テーブルにカラムを追加
<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class AddClosureColumnsOnComments extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table('comments', function (Blueprint $table) { $table->integer('parent_id')->unsigned()->nullable(); $table->integer('position', false, true); $table->integer('real_depth', false, true); $table->foreign('parent_id') ->references('id') ->on('comments') ->onDelete('set null'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('comments', function (Blueprint $table) { $table->dropColumn('parent_id'); $table->dropColumn('position'); $table->dropColumn('real_depth'); }); } }
3. モデルの作成
既存のモデル(Comment)を修正するほか、Closure Table 用のモデルを 3 つ追加します
Comment.php の修正
<?php namespace App; use Franzose\ClosureTable\Models\Entity; class Comment extends Entity implements CommentInterface { /** * The table associated with the model. * * @var string */ protected $table = 'comments'; /** * ClosureTable model instance. * * @var CommentClosure */ protected $closure = 'App\CommentClosure'; //複数代入を許可する項目 protected $fillable = ['name', 'content']; (以下略) }
use を追加するのと、$closure を追加するのが主な変更ですが、1点注意しなければならないのが複数代入の設定です。
ご存知のように、モデルには fillable か guarded のどちらかの属性を設定する必要がありますが
Closure Table を利用するためには、fillable で設定する必要があります。
というのも、Closure Table で利用するカラムに値を追加できるように
$this->fillable(array_merge($this->getFillable(), [$position, $depth]));
という処理を行っているためです。新規作成の場合にはこのあたり気にする必要はありませんが、既存のモデルを使う場合、注意が必要です。
CommentClosure.php の作成
<?php namespace App; use Franzose\ClosureTable\Models\ClosureTable; class CommentClosure extends ClosureTable implements CommentClosureInterface { /** * The table associated with the model. * * @var string */ protected $table = 'comment_closures'; }
CommentClosureInterface.php の作成
<?php namespace App; use Franzose\ClosureTable\Contracts\ClosureTableInterface; interface CommentClosureInterface extends ClosureTableInterface { }
CommentInterface.php の作成
<?php namespace App; use Franzose\ClosureTable\Contracts\EntityInterface; interface CommentInterface extends EntityInterface { }
4. 初期データの投入
新規作成の場合は必要ないのですが、既存データを利用する場合、comments.position に値を入れるとともに、comment_closures テーブルに初期データを投入する必要があります。
comments.position は同じ階層にあるデータの並び順を表すデータで、他のデータと重複しないように設定する必要があります。
また、comment_closure テーブルのほうには、いったんすべてのデータが最上位の階層に並んでいるようなデータを投入します。
手っ取り早いのは、プライマリキーに設定されている comments.id を使う方法で
update comments set position = id; insert into comment_closures (`ancestor`, `descendant`, depth) select id, id, 0 from comments;
こんな感じで設定しておけば大丈夫です
これで Closure Table が使えるようになると思います。それ専用に作られているだけあって、データの操作や取得はかなり便利に行うことができます。お試しください!
Laravel のセッションを非 Laravel のプロジェクトと共有する
基本的な手順は kojirockさんのこちらの記事 を参考にさせていただきました。ありがとうございます。
1. 必要なライブラリをインストール
$ composer require illuminate/encryption
2. Laravel用sessionテーブル作成
3. Laravel側session設定
Laravel側の設定なので、このあたりを参照してください
4. Laravel側session設定を共有したいプロジェクトで定数として設定
例えばこんな感じ
<?php define("LARAVEL_APP_KEY", "base64:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); define("SESSION_NAME", "laravel_session"); define("SESSION_TIME", time() + (120 * 60)); define("SESSION_PATH", "/"); define("SESSION_DOMAIN", null); define("SESSION_SECURE", false); define("SESSION_HTTP_ONLY", true); define("LARAVEL_CIPHER", "AES-256-CBC"); define("DBNAME", "dbname"); define("DBHOST", "127.0.0.1"); define("DBUSER", "hoge"); define("DBPASSWORD", "fuga");
5. SessionHandlerを作成
kojirock さんの記事を参考にしつつ、session_regenerate_id に対応できるように create_sid メソッドを追加しています
<?php namespace MyApp\Session; use Illuminate\Encryption\Encrypter; use Illuminate\Support\Str; class AppSessionHandler implements \SessionHandlerInterface { /** * @var string */ protected $decryptSessionId; /** * @var \PDO */ protected $pdo; /** * AppSessionHandler constructor. * @param string $decryptSessionId * @param \PDO $pdo */ public function __construct($decryptSessionId, \PDO $pdo) { $this->decryptSessionId = $decryptSessionId; $this->pdo = $pdo; } /** * @param string $savePath * @param string $name * @return bool */ public function open($savePath, $name) { return true; } /** * @param string $sessionId * @return string */ public function read($sessionId) { $stmt = $this->pdo->prepare("SELECT payload FROM sessions WHERE id = :id"); $stmt->bindValue(":id", $this->decryptSessionId); $stmt->execute(); $results = $stmt->fetchColumn(); $stmt->closeCursor(); return is_null($results) ? '' : base64_decode($results); } /** * @param string $sessionId * @param string $sessionData * @return bool */ public function write($sessionId, $sessionData) { $sql = <<<SQL INSERT INTO sessions (id, ip_address, user_agent, payload, last_activity) VALUES(:id, :ip_address, :user_agent, :payload, :last_activity) ON DUPLICATE KEY UPDATE last_activity = :last_activity, payload = :payload SQL; if ($this->isEncryptSessionId($sessionId)) { $sessionId = $this->decryptSessionId($sessionId); } $payload = $this->getSavePayload($sessionId, $sessionData); $stmt = $this->pdo->prepare($sql); $stmt->bindValue(":id", $sessionId); $stmt->bindValue(":ip_address", $_SERVER['REMOTE_ADDR']); $stmt->bindValue(":user_agent", $_SERVER['HTTP_USER_AGENT']); $stmt->bindValue(":payload", base64_encode(serialize($payload))); $stmt->bindValue(":last_activity", time()); $stmt->execute(); $results = $stmt->rowCount(); $stmt->closeCursor(); return ($results) ? true : false; } /** * @param string $sessionId * @return bool */ public function destroy($sessionId) { $stmt = $this->pdo->prepare("DELETE FROM sessions WHERE id = :id"); $stmt->bindValue(":id", $this->decryptSessionId); $stmt->execute(); $stmt->closeCursor(); return true; } /** * @return bool */ public function close() { return true; } /** * @param int $maxlifetime * @return bool */ public function gc($maxlifetime) { $stmt = $this->pdo->prepare("DELETE FROM sessions WHERE last_activity < :lastActivity"); $stmt->bindValue(":lastActivity", $maxlifetime); $stmt->execute(); $stmt->closeCursor(); return true; } /** * 新しい sessionIDの作成 * @return string */ public function create_sid() { $sessionId = Str::random(40); return $sessionId; } /** * @param string $sessionId * @param string $sessionData * @return array */ protected function getSavePayload($sessionId, $sessionData) { // DB から保存されているデータを取得 $registeredPayload = $this->read($sessionId); if (strlen($registeredPayload) >= 1) { $registeredPayload = unserialize($registeredPayload); } else { // 保存されているデータがなければ初期化 $registeredPayload = $this->initPayload(); } if (strlen($sessionData) >= 1) { // セッションとして登録したいデータがあれば上書き $payload = unserialize($sessionData); } else { // セッションとして登録したいデータがなければデータを保持 $payload = $registeredPayload; } $payload['_previous'] = ['url' => $_SERVER['HTTP_REFERER']]; return $payload; } /** * payloadの初期化 * @return array */ protected function initPayload() { return $payload = [ '_token' => Str::random(40), '_flash' => ['old' => [], 'new' => []] ]; } /** * 暗号化された sessionID かどうかの判別 * @param $sessionId * @return bool */ protected function isEncryptSessionId($sessionId) { if (is_string($sessionId) && ctype_alnum($sessionId) && strlen($sessionId) === 40) { return false; } else { return true; } } /** * 暗号化された sessionID の復号 * @param $sessionId * @return string */ protected function decryptSessionId($sessionId) { $encryptValue = base64_decode(substr(LARAVEL_APP_KEY, 7)); $encrypter = new Encrypter($encryptValue, LARAVEL_CIPHER); $decryptSessionId = $encrypter->decrypt($sessionId); return $decryptSessionId; } }
6. カスタムセッションハンドラの設定
4. で設定した定数を /var/www/config.php に、5. で設定したSessionHandlerを /var/www/class_session_handler.php に置いていたとして
<?php require_once '/var/www/config.php'; require_once '/var/www/class_session_handler.php'; use Illuminate\Encryption\Encrypter; use Illuminate\Support\Str; use MyApp\Session; ini_set('session.serialize_handler', 'php_serialize'); $encryptValue = base64_decode(substr(LARAVEL_APP_KEY, 7)); $encrypter = new Encrypter($encryptValue, LARAVEL_CIPHER); if (!isset($_COOKIE[SESSION_NAME])) { $sessionId = Str::random(40); $encryptSessionId = $encrypter->encrypt($sessionId); setcookie(SESSION_NAME, $encryptSessionId, SESSION_TIME, SESSION_PATH, SESSION_DOMAIN, SESSION_SECURE, SESSION_HTTP_ONLY); } else { if (is_string($_COOKIE[SESSION_NAME]) && ctype_alnum($_COOKIE[SESSION_NAME]) && strlen($_COOKIE[SESSION_NAME]) === 40) { // regenerate された session_id のときは session_id を暗号化して再格納 $encryptSessionId = $encrypter->encrypt($_COOKIE[SESSION_NAME]); setcookie(SESSION_NAME, $encryptSessionId, SESSION_TIME, SESSION_PATH, SESSION_DOMAIN, SESSION_SECURE, SESSION_HTTP_ONLY); $decryptSessionId = $_COOKIE[SESSION_NAME]; } else { $decryptSessionId = $encrypter->decrypt($_COOKIE[SESSION_NAME]); } $dsn = 'mysql:host=' . DBHOST . ';dbname=' . DBNAME; $pdo = new PDO($dsn, DBUSER, DBPASSWORD); $sessionHandler = new Session\AppSessionHandler($decryptSessionId, $pdo); session_name(SESSION_NAME); session_set_save_handler($sessionHandler, true); }
このあとに session_start() を呼び出せば、Laravel と非 Laravel の双方で読み書きできるセッションが作れると思います
Laravel クエリビルダの whereRaw メソッドでプレースホルダを使う
Laravel 5.5 のドキュメントには記載があるのに(日本語版、英語版)、それ以前のドキュメントには、なぜか記載がないのですが、クエリビルダの whereRaw メソッドでプレースホルダを使うときは、名前付きプレースホルダではなく、疑問符プレースホルダを使う必要があります。
$orders = DB::table('orders') ->whereRaw('price > IF(state = "TX", ?, 100)', [200]) ->get();
名前付きプレースホルダを使うと、以下のエラーが出ます
Uncaught exception 'PDOException' with message 'SQLSTATE[HY093]: Invalid parameter number: mixed named and positional parameters'
こちらでもう少し詳しく説明しました。gn-office.pro