こんばんは。
簡単な報告になります。
先日上げた記事でobject-mapperをとりあげましたが、その記事内で例で作成した CarbonTypeCasterと軽微な機能追加を マージしてもらいました。
実際にマージしてもらったPRは以下です。
小さいことですがOSSに貢献できるのって、すごく良いことした気分です。
満員電車でお年寄りに席を譲った感じです。
簡単な報告になりますが、以上です。
こんばんは。
簡単な報告になります。
先日上げた記事でobject-mapperをとりあげましたが、その記事内で例で作成した CarbonTypeCasterと軽微な機能追加を マージしてもらいました。
実際にマージしてもらったPRは以下です。
小さいことですがOSSに貢献できるのって、すごく良いことした気分です。
満員電車でお年寄りに席を譲った感じです。
簡単な報告になりますが、以上です。
こんばんは。
最近やっとフレームワークからドメイン(関心事)の切り離しがわかってきた僕です。
切り離しに関して僕的に一番めんどくさいのがORM(データベース)です。
LaravelだとEloquentです。
EloquentデータからEntityへのマッピングで結構泥臭くさくやらないとだめですよね。
新原さんのコアレイヤパターンとかでも以下みたいな感じで、入れ直しています。
データの数が少ない場合は問題ないんですが、10〜15個とか膨大な数になっちゃうとケアレスミスも起きやすいし、何よりめんどくさい。
この解決方法ないかな〜と考えていたら、自分の師匠である @polidog さんが作ってくれました。
例えばこんなEntityクラスがあるとします。
<?php namespace Kojirock5260\Entity; class User { /** * @var int */ private $id; /** * @var string */ private $email; /** * @var UserProfile */ private $userProfile; }
<?php namespace Kojirock5260\Entity; class UserProfile { /** * @var int */ private $id; /** * @var int */ private $userId; /** * @var string */ private $name; /** * @var \DateTime */ private $birthday; /** * @var int */ private $sex; }
先程あげたコアレイヤパターン的な感じで行くならば、 User.php に以下みたいなメソッドを生やす感じになると思います。
public static function ofByArray(array $values) { return new self( $values['id'] ?? 0, $values['email'] ?? '', UserProfile::ofByArray($values['userProfile'] ?? []) ); }
このあたりを object-mapperで書き直すとこうなります。
<?php $parser = new \Helicon\ObjectTypeParser\Parser(); $resolver = new \Helicon\TypeConverter\Resolver(); $hydrator = new \Zend\Hydrator\ReflectionHydrator(); $resolver->addConverter(new \Helicon\TypeConverter\TypeCaster\ScalarTypeCaster()); $resolver->addConverter(new \Helicon\TypeConverter\TypeCaster\DateTimeCaster()); $resolver->addConverter(new \Helicon\TypeConverter\TypeCaster\ClassTypeCaster($resolver, $parser, $hydrator)); $converter = new \Helicon\TypeConverter\Converter($resolver); $mapper = new \Helicon\ObjectMapper\ObjectMapper($converter, $parser, $hydrator); $user = ($mapper)($data, \Kojirock5260\Entity\User::class);
$ php example.php /path/to/example.php:30: class Kojirock5260\Entity\User#61 (3) { private $id => int(100) private $email => string(17) "example@gmail.com" private $userProfile => class Kojirock5260\Entity\UserProfile#45 (5) { private $id => int(1) private $userId => int(1) private $name => string(8) "kojirock" private $birthday => class DateTime#64 (3) { public $date => string(26) "1990-01-01 00:00:00.000000" public $timezone_type => int(3) public $timezone => string(3) "UTC" } private $sex => int(2) } }
実質的なマッピングは以下の行です。
$user = ($mapper)($data, \Kojirock5260\Entity\User::class);
前段でもろもろ準備が必要ですけど、その点はうまくメソッド化すれば問題ないと思います。
DocCommentでマッピングが解決できるのがとにかく素敵ですよね。
拡張性が高いもの魅力の一つです。
例えば \DateTime
ではなく Carbonを使いたいとなった場合、オリジナルのTypeCasterを作成することで解決できます。
<?php namespace Kojirock5260\Caster; use Carbon\Carbon; use Carbon\CarbonImmutable; use Helicon\TypeConverter\TypeCaster\TypeCasterInterface; class CarbonTypeCaster implements TypeCasterInterface { public function convert($value, string $type) { return new $type($value); } public function supports(string $type): bool { return Carbon::class === $type || CarbonImmutable::class === $type; } }
UserProfile.php
の $birthday
のDocCommentを変更します。
/** * @var \DateTime */ private $birthday; ↓ /** * @var CarbonImmutable */ private $birthday;
セットしていたDateTimeTypeCasterをCarbonTypeCasterへ差し替えます。
$resolver->addConverter(new \Helicon\TypeConverter\TypeCaster\DateTimeCaster()); ↓ $resolver->addConverter(new \Kojirock5260\Caster\CarbonTypeCaster());
$ php example.php /path/to/example.php:30: class Kojirock5260\Entity\User#61 (3) { private $id => int(100) private $email => string(17) "example@gmail.com" private $userProfile => class Kojirock5260\Entity\UserProfile#45 (5) { private $id => int(1) private $userId => int(1) private $name => string(8) "kojirock" private $birthday => class Carbon\CarbonImmutable#64 (3) { public $date => string(26) "1990-01-01 00:00:00.000000" public $timezone_type => int(3) public $timezone => string(3) "UTC" } private $sex => int(2) } }
$birthday
がCarbonImmutableに差し替わっています。
例えば email
とかは、ValueObjectのように最小単位の値クラスを作って管理したいです。
以下みたいなValueObjectがあるとします。
<?php namespace Kojirock5260\ValueObject; class MailAddress implements ValueObjectInterface { /** * @var string */ private $value; /** * MailAddress constructor. * @param string $value * @throws \InvalidArgumentException */ public function __construct(string $value) { $this->value = $value; if (!$this->valid()) { throw new \InvalidArgumentException('invalid mail address'); } } /** * @return string */ public function value():string { return $this->value; } /** * @return bool */ public function valid():bool { $result = filter_var($this->value, FILTER_VALIDATE_EMAIL); return $result === false ? false : true; } }
この辺どうやるのだろう...。
と思って polidog師匠に相談すると F1レベルの速さで返事がコード付きで返ってきました。
こちらもCarbon同様拡張でどうにでもなるようです。
<?php namespace Kojirock5260\Caster; use Helicon\ObjectTypeParser\Parser; use Helicon\TypeConverter\Resolver; use Helicon\TypeConverter\TypeCaster\TypeCasterInterface; use Kojirock5260\ValueObject\ValueObjectInterface; use Zend\Hydrator\ReflectionHydrator; class ValueObjectTypeCaster implements TypeCasterInterface { /** * @var Resolver */ private $resolver; /** * @var Parser */ private $parser; /** * @var ReflectionHydrator */ private $reflectionHydrator; /** * @param Resolver $resolver * @param Parser $parser * @param ReflectionHydrator $reflectionHydrator */ public function __construct(Resolver $resolver, Parser $parser, ReflectionHydrator $reflectionHydrator) { $this->resolver = $resolver; $this->parser = $parser; $this->reflectionHydrator = $reflectionHydrator; } /** * @param $value * @param string $type * @return mixed|object|\Zend\Hydrator\object * @throws \ReflectionException */ public function convert($value, string $type) { $refClass = new \ReflectionClass($type); $schemas = ($this->parser)($type); $type = $schemas['value']['type']; $convertedValue = $this->resolver->resolve($type)->convert($value, $type); return $this->reflectionHydrator->hydrate([ 'value' => $convertedValue, ], $refClass->newInstanceWithoutConstructor()); } /** * @param string $type * @return bool * @throws \ReflectionException */ public function supports(string $type): bool { $refClass = new \ReflectionClass($type); if ($refClass->isSubclassOf(ValueObjectInterface::class)) { return true; } return false; } }
User.php
の email
のDocCommentを修正します。
/** * @var string */ private $email; ↓ /** * @var MailAddress */ private $email;
先程作ったValueObjectTypeCasterをConverterとして追加します。
$resolver->addConverter(new \Kojirock5260\Caster\ValueObjectTypeCaster($resolver, $parser, $hydrator));
$ php example.php /path/to/example.php:31: class Kojirock5260\Entity\User#62 (3) { private $id => int(100) private $email => class Kojirock5260\ValueObject\MailAddress#49 (1) { private $value => string(18) "example@gmail.com" } private $userProfile => class Kojirock5260\Entity\UserProfile#54 (5) { private $id => int(1) private $userId => int(1) private $name => string(8) "kojirock" private $birthday => class Carbon\CarbonImmutable#68 (3) { public $date => string(26) "1990-01-01 00:00:00.000000" public $timezone_type => int(3) public $timezone => string(3) "UTC" } private $sex => int(2) } }
email
が指定のValueObjectのクラス MailAddress
としてマッピングされています!
こんな感じでマッピングが狙ったとおりにきれいにマッピングされるのが object-mapperです。
早速自分のプロジェクトでも推してみようと思います。
今回試したサンプルプロジェクトはgithubに上げておきます。
polidog師匠ありがとうございました。
こんばんは。
ちょうあっさりした、かつ、基本的なものですが、備忘録のために残しときます。
タイトルの通り、 hasMany関係のテーブルから1件以上のデータが登録あるものだけ取得する方法です。
さらに、そのテーブルのデータは 登録日降順で取得するものとします。
以下みたいなテーブルがあるとします。
CREATE TABLE `categories` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PrimaryKey', `name` varchar(256) NOT NULL DEFAULT '' COMMENT 'カテゴリ名', `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '登録日', `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新日', PRIMARY KEY (`id`), KEY `name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT 'カテゴリ管理テーブル'; CREATE TABLE `items` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PrimaryKey', `category_id` int(11) unsigned NOT NULL COMMENT 'カテゴリID', `name` varchar(256) NOT NULL DEFAULT '' COMMENT 'アイテム名', `price` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '価格', `delivery_cost` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '配送料', `stock` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '在庫', `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '登録日', `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新日', PRIMARY KEY (`id`), KEY `userId` (`name`), CONSTRAINT `items_fk_1` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT 'アイテム管理テーブル';
Eloquentは以下みたいな感じ?
<?php class EloquentCategory extends Model { protected $table = 'categories'; /** * @return HasMany */ public function items(): HasMany { return $this->hasMany(EloquentItem::class); } } class EloquentItem extends Model { protected $table = 'items'; /** * @return BelongsTo */ public function area(): BelongsTo { return $this->belongsTo(EloquentCategory::class); } }
Itemが1件以上登録があるCategoryを取得する方法は以下
<?php /** * @return array */ public function findCategories(): array { return EloquentCategory::query() ->with(['items' => function (HasMany $query): void { $query->orderBy('created_at', 'DESC'); }]) ->orderBy('id', 'asc') ->get()->filter(function ($item) { return ($item->items->count() >= 1); })->toArray(); }
これで、アイテムが1件以上登録のあるカテゴリのcollectionが取得できます。
withの小技と、collectionのfilterを組み合わせただけなんですが、こういうところって知ってないと使えないですよね。
調べるまでは泥臭く foreach で再現やる感じかな?と想像してました。
今日はとりあえずこんな感じです。
12月チャレンジのために来週からは仕込み期間です。
がんばります。
風邪引いてダウン中です。
kojirockです。
今回はめちゃめちゃ簡単なものです。
よくある検索機能で検索パラメータが複数あるようなのがあると思います。
今回、別ページでもその検索パラメータが使えるような要件がありました。
それぞれのAPIで検索パラメータによってwhere句をつけるif文の塊をコピペしてたのですが、criteria的な感じで共通化できないか?というお話をチームメンバーの方に相談されて、調べてやってみました。
結果、実態はscopeですが、なんとなく出来たので、忘れないようにブログにします。
こちらのレポジトリを参考にさせていただきました。レポジトリには他にもAllOfCriteriaCollection.phpなどもあったのですが、もっと薄くてよかったので、Criteriaとして使えるものだけピンポイントだけ使わせてもらいました。
<?php namespace App\Criterias; use Illuminate\Database\Eloquent\Builder; interface CriteriaInterface { /** * @param Builder $query */ public function append(Builder $query); }
<?php namespace App\Criterias; use App\Eloquents\Builder\CustomBuilder; use Illuminate\Database\Eloquent\Builder; /** * Trait CriteriaTrait * @package App\Criterias * @method CustomBuilder query() * @method CustomBuilder newQuery() * @method CustomBuilder newModelQuery() */ trait CriteriaTrait { /** * @param Builder $query * @param CriteriaInterface $criteria * @return Builder */ public function scopeCriteria(Builder $query, CriteriaInterface $criteria): Builder { $criteria->append($query); return $query; } /** * Create a new Eloquent query builder for the model. * * @param \Illuminate\Database\Query\Builder $query * @return \Illuminate\Database\Eloquent\Builder|static */ public function newEloquentBuilder($query): Builder { return new CustomBuilder($query); } }
<?php namespace App\Criterias; use Illuminate\Database\Eloquent\Builder; class SearchUserCriteria implements CriteriaInterface { /** * @var array */ private $parameters; /** * SearchUserCriteria constructor. * @param array $parameters */ public function __construct(array $parameters) { $this->parameters = $parameters; } /** * @param Builder $query */ public function append(Builder $query) { // 検索パラメータによってwhere句を追加 } }
<?php declare(strict_types=1); namespace App\Eloquents\Builder; use App\Criterias\CriteriaInterface; use Illuminate\Container\Container; use Illuminate\Database\Concerns\BuildsQueries; use Illuminate\Database\Eloquent\Builder; /** * Class CustomCriteriaBuilder * @package App\Eloquents\Builder * @method \Illuminate\Database\Query\Builder criteria(CriteriaInterface $criteria) */ class CustomBuilder extends Builder { }
<?php declare(strict_types=1); namespace App\Eloquents; use App\Criterias\CriteriaTrait; use Illuminate\Database\Eloquent\Model; /** * Class EloquentUser * @package App\Eloquents */ class EloquentUser extends Model { use CriteriaTrait; protected $table = 'users'; protected $fillable = ['email', 'password']; public function searchUsers(array $parameters): array { return self::query() ->criteria(new SearchUserCriteria($parameters)) ->paginate(15); } }
補完を効かせなくてよければ、CustomBuilderを作る必要はないです。
自分の場合はストレスだったので作りました。
あと、CustomBuilderつくっとけば、たとえば Paginatorの返却クラスも上書きできたりするので、便利でした。
もっと他に良さげなやり方あれば教えて下さい。。。
こんばんは。
先週は自分の家の引っ越し、今週は両親の引越の手伝いで、ブログが全く書けていなく、WBEW退会待ったなしになっている私です。
今回はお世話になっているお仕事で、 以前ブログにも書いた tbls を使ってドキュメントを自動生成した話をブログに残します。
退会が迫っているので一気になぐり書きしたので、間違ってる箇所あるかもです。。。
今回は circleciを使って、developブランチにマージされた段階で tblsの自動生成が走るようにしました。
たぶん自動更新というよりは、 tbls diff
を使って、ドキュメント更新を促すというのが本来の使い方なのかなと思っていますが、今回は circleci で pushまでしちゃってます。
ちなみに tblsの以前の記事はこちらです。
circleciの config.ymlは以下のとおりです。 ブログ化するために消しちゃったのですが、本来はphpunitを実行するための記述とかもあるので、少し冗長な感じになっております。
Laravelプロジェクトのマイグレーションを実行して、circleciのDBにテーブルを作成して、そこからtblsでドキュメントを作成しています。
前提として、circleciで登録している githubのssh keyに Allow write access
がついている必要があります。
version: 2.1 executors: default: docker: - image: circleci/php:7.3.5-apache - image: circleci/mysql:5.7-ram commands: setup_infrastructure: steps: - run: sudo apt update - run: sudo apt-get install -y libpng-dev graphviz - run: sudo docker-php-ext-install pdo_mysql gd setup_application: steps: - restore_cache: keys: - v1-dependencies-{{ checksum "composer.json" }} - v1-dependencies- - run: composer install -n --prefer-dist - save_cache: key: v1-dependencies-{{ checksum "composer.json" }} paths: - ./vendor - run: php artisan config:clear execute_tbls: steps: - run: command: | cd ./vendor wget https://github.com/k1LoW/tbls/releases/download/v1.19.0/tbls_v1.19.0_linux_amd64.tar.gz tar zxvf tbls_v1.19.0_linux_amd64.tar.gz ./tbls doc mysql://$DB_USERNAME:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_DATABASE ../docs/schema -f jobs: tbls: executor: name: default environment: - APP_DEBUG: false - APP_ENV: develop - DB_DATABASE: circle_test - DB_HOST: 127.0.0.1 - DB_USERNAME: root - DB_PASSWORD: "" - DB_PORT: 3306 - MYSQL_ALLOW_EMPTY_PASSWORD: true steps: - checkout - add_ssh_keys: fingerprints: - "4d:f1:9a:38:98:3a:cb:3d:c6:c9:e7:bd:75:57:fe:14" - setup_infrastructure - setup_application - run: php artisan migrate:fresh - execute_tbls - run: ./.circleci/push_database_documents.sh develop workflows: version: 2 build: jobs: - tbls: filters: branches: only: develop
PRにDB構造の変化がない場合は、git commit 時にエラーステータスが返ってきちゃうので、そのへんの判断は シェルでやっちゃいました。
MODIFY_FILE_COUNT=`git status | grep docs/schema | wc -l` if [ $MODIFY_FILE_COUNT -ne 0 ]; then git config user.email "example_kojircok@gmail.com" git config user.name "circleci" git add docs/schema git commit -m 'update database documents' git push origin $1 fi
現状は問題は特に出ておらず、いい感じです。
WBEW発足者として退会になるわけにはいかないという理由でなんとか書きました。
マジで危なかった。
もっとこうしたほうがいいとかあれば教えてもらいたいです!
現場からは以上です。
あと 光の開通工事までレンタルで貸し出された Softbank Air死ぬほど重い。
こんばんは。
今回はポエム的なブログです。
ふとしたことがきっかけで立ち上げた write-blog-every-week が一年を迎えていました。
きっかけは皆さんおなじみ こちらのpodcastでした。
とりあえずやってみようという思いでやり始めたら、あれよあれよという間に人が増えて、一時期は40人近くになり人数制限入れないとやばくね?みたいなところにまでなっていました。
現在は20人程度で落ち着いています。
せっかく1年たったので、簡単に振り返ってみたいと思います。
昔から、何をしても長続きしなかった自分が1年間も続けられたというところが、まず嬉しいの一言です。
これはやはり、WBEWに入ってくれた皆さんに刺激されたところが大きいと思います。
強制的に勉強するという癖がついたり、ブログにするために、メモ代わりにログを残すことを癖付けられたりしました。
ブログ書くために勉強すんのかよ!逆だろ!っていう感じかもしれませんが、どっちが先でも全然問題ないです。だって自主的に勉強してるんだもの。
いろんな方のブログを見たり、技術的相談などをslack内で投げたりなどしているうちに、いろんなエンジニアさんと仲良くなれました。
特に@yoshitaku_jpさん @budougumi0617さん @kdnaktさんは、立ち上げ初期から色々と相談に乗ってくれたり、飲み会行ったり、輪読会したりと大変お世話になりました。
人数が増えてきたときの対応を一度失敗したと感じています。
ブログを書くことにある程度、強制力をもたせたい(自分のために)ところがあったので、強制退会ルールを敷いていたのですが、二転三転して、ルールがコロコロと変わってしまう事がありました。
そのせいで、悲しいことに脱退になってしまった方もいて、本当に申し訳なかったと思っています。
まさに今もそうなんですが、プライベートと仕事が忙しく、とにかく脱退しないために低品質のブログを上げることが続いてしまいました。
忙しくても、計画性が大事だなと思いました。
去年出来たので、ことしもできればやりたいです。
12月に是非いければ...!
振り返りとしてはこんな感じです。
無事2年目に突入したWrite-Blog-Every-Weekをどうぞよろしく。
challenge-every-monthという1ヶ月で目標を決めてチャレンジするというチームも立ち上げているので、そちらも WBEW 同様頑張らねば...w
こんばんは。
今回も短いですがあげます。
今フリーランスでお仕事いただいているところでは CDNにCloudFrontを使用しています。
jsなりcssなりを上げた際に毎回キャッシュクリアしているのですが、 /css/*
とか /js/*
とかって感じでガッツリ全部キャッシュクリアしちゃっているようでした。
今回、なんとかうまく狙ったファイルだけクリアできないかなーと思い、頑張ってみました。
修正前までは、過去の負債で何故か、開発用レポジトリと、本番用レポジトリというように、2つのレポジトリがありました。
開発用レポジトリで js,css関連のbuildをおこなったり、相対パスをcloudfrontからの絶対パスに書き換えたりしたものを、まるママ rsyncで本番用に移すという力技で作っておりました。
そのさい、コピーするファイル群の中に、css, jsが存在していれば aws cloudfront create-invalidation
を叩いてキャッシュクリアしていました。
今回、そのあたりも一つのレポジトリに統一したので、若干アプローチが変わります。
まず、必ずリリースタグを切るようにしたので、最新タグを取得するように git describe --tags
を利用します。
そして、git logをこねくり回して、「編集されたjs,cssファイル」を抜き出しました。
# distribution-id DISTRIBUTION_ID=********** # 最新tagを取得 GIT_LATEST_TAG=`git describe --tags` # CloudFront削除(CSS,JS) MODIFY_FILES=`git log --name-status -n 1 --oneline ${GIT_LATEST_TAG} | grep -E '(.js$|.css)' | grep -E '^M' | awk '{print $2}' | awk -F '/' '{print substr($0, 12)}'` if [ -n "$MODIFY_FILES" ]; then aws cloudfront create-invalidation --distribution-id $DISTRIBUTION_ID --paths $MODIFY_FILES fi
これで、今回反映予定で、修正があったjs,cssファイルだけをキャッシュクリアできます。
$ ./cloudfront.sh { "Location": "https://cloudfront.amazonaws.com/YYYY-MM-DD/distribution/XXXXXXXXXXX/invalidation/ZZZZZZZZZZZ", "Invalidation": { "Id": "ZZZZZZZZZZZ", "Status": "InProgress", "CreateTime": "2019-09-15T19:22:18.038Z", "InvalidationBatch": { "Paths": { "Quantity": 5, "Items": [ "/js/hoge.js", "/css/fuga.css", "/css/fuga/piyo.css", "/css/fugafuga/piyopiyo.css", "/css/hoge.css" ] }, "CallerReference": "cli-1111111111111-222222" } } }
シェルスクリプトはいつまでたっても苦手です。