もがき系プログラマの日常

もがき系エンジニアの勉強したこと、日常のこと、気になっている技術、備忘録などを紹介するブログです。

GatsbyJSさわってみた

はじめに

こんばんは。今回は積本消化月間 vol.1で手を進みながら勉強した、GatsbyJSをやってみたログ的なものをきれいに整形してブログにしました。

WEB+DB PRESS Vol.108

WEB+DB PRESS Vol.108

  • 作者: 中野暁人,山本浩平,大和田純,曽根壮大,ZOZOTOWNリプレースチーム,権守健嗣,茨木暢仁,松井菜穂子,新多真琴,laiso,豊田啓介,藤原俊一郎,牧大輔,向井咲人,大島一将,上川慶,末永恭正,久保田祐史,星北斗,池田拓司,竹馬光太郎,粕谷大輔,WEB+DB PRESS編集部
  • 出版社/メーカー: 技術評論社
  • 発売日: 2018/12/22
  • メディア: 単行本
  • この商品を含むブログを見る

GastbyJSは、自分が所属しているWriteBlogEveryWeekに、Contributorの @mottox2 さんが同じく所属しており、以前少し話題が出たときに気になってましたので、ちょうどよかったですw

twitter.com

今回はやってみたログなので、プログラムコードに関しては、本で紹介されている内容を忠実に再現しています。

本題

1. GatsbyJS CLIの準備と初期設定

インストール
$ yarn global add gatsby-cli
.
..
...
✨  Done in 70.60s.
インストール確認
$ gatsby --version
Gatsby has started collecting anonymous usage analytics to help improve Gatsby for all users.
If you'd like to opt-out, you can use `gatsby telemetry --disable`
To learn more, checkout http://gatsby.dev/telemetry
2.5.6

本では、バージョンが2.4.4でしたがバージョンが上がって2.5.6が入りました。 また、妙なメッセージが出たので何だと調べると、Gastbyが、改善などのために情報を集めますみたいな話のようです。 情報を送信したくない場合は gatsby telemetry --disable を実行しなさいとうことだそうです。

こちらのメッセージは二回目以降は出なくなりました。

スターターキットをインストール
$ gatsby new gatsby_test https://github.com/gatsbyjs/gatsby-starter-hello-world
info Creating new site from git: https://github.com/gatsbyjs/gatsby-starter-hello-world.git
Cloning into 'gatsby_test'...
remote: Enumerating objects: 547, done.
remote: Total 547 (delta 0), reused 0 (delta 0), pack-reused 547
Receiving objects: 100% (547/547), 1.72 MiB | 486.00 KiB/s, done.
Resolving deltas: 100% (355/355), done.
success Created starter directory layout
info Installing packages...
yarn install v1.12.3
warning package-lock.json found. Your project contains lock files generated by tools other than Yarn. It is advised not to mix package managers in order to avoid resolution inconsistencies caused by unsynchronized lock files. To clear this warning, remove package-lock.json.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
warning "gatsby > eslint-plugin-graphql@2.1.1" has incorrect peer dependency "graphql@^0.12.0 || ^0.13.0".
warning "gatsby > express-graphql@0.6.12" has incorrect peer dependency "graphql@^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0".
warning "gatsby > graphql-tools@3.1.1" has incorrect peer dependency "graphql@^0.13.0".
warning "gatsby > pnp-webpack-plugin > ts-pnp@1.0.1" has unmet peer dependency "typescript@*".
[4/4] 📃  Building fresh packages...
✨  Done in 191.23s.
info Initialising git in gatsby_test
Initialized empty Git repository in /path/to/gatsby_test/.git/
info Create initial git commit in gatsby_test
テストプロジェクトインストール確認
$ cd gatsby_test
$ ll
total 1712
-rw-r--r--     1 kojirock  staff   1.0K  4 10 00:46 LICENSE
-rw-r--r--     1 kojirock  staff   5.2K  4 10 00:46 README.md
drwxr-xr-x  1000 kojirock  staff    31K  4 10 00:49 node_modules
-rw-r--r--     1 kojirock  staff   452K  4 10 00:46 package-lock.json
-rw-r--r--     1 kojirock  staff   770B  4 10 00:46 package.json
drwxr-xr-x     3 kojirock  staff    96B  4 10 00:46 src
drwxr-xr-x     3 kojirock  staff    96B  4 10 00:46 static
-rw-r--r--     1 kojirock  staff   382K  4 10 00:46 yarn.lock
立ち上げて実行確認
$ gatsby develop
success open and validate gatsby-configs — 0.007 s
success load plugins — 0.044 s
success onPreInit — 0.007 s
success initialize cache — 0.006 s
success copy gatsby files — 0.131 s
success onPreBootstrap — 0.016 s
success source and transform nodes — 0.014 s
success building schema — 0.122 s
success createPages — 0.000 s
success createPagesStatefully — 0.049 s
success onPreExtractQueries — 0.000 s
success update schema — 0.026 s
success extract queries from components — 0.014 s
success run graphql queries — 0.011 s — 2/2 211.74 queries/second
success write out page data — 0.005 s
success write out redirect data — 0.001 s
success onPostBootstrap — 0.000 s

info bootstrap finished - 5.132 s

 DONE  Compiled successfully in 4274ms                                                                                                                                                                                                                                                                                00:52:35


You can now view gatsby-starter-hello-world in the browser.

  http://localhost:8000/

View GraphiQL, an in-browser IDE, to explore your site's data and schema

  http://localhost:8000/___graphql

Note that the development build is not optimized.
To create a production build, use npm run build

ℹ 「wdm」: 
ℹ 「wdm」: Compiled successfully.

f:id:kojirooooocks:20190410021551p:plain

f:id:kojirooooocks:20190410021605p:plain

起動できました。

http://localhost:8000/___graphql というURLも表示されたのですが、こちらはGatsbyJSで使用されているGraphQLのURLです。

2. ページ追加 & 修正

aboutページを作成
// src/pages/about.js 

import React from "react"
export default () => <div>aboutページ</div>

f:id:kojirooooocks:20190410021617p:plain

問題なく表示されました。

componentsを追加
// src/components/Header.js 

import React from 'react';
export default props => <h1 style={{ color: `red` }}>{props.title}</h1>
componentを各ページで使用
// src/page/index.js 

import React from "react"
import { Link } from 'gatsby'
import Header from '../components/Header'
export default () => (
  <div>
    <Link to="/about/">about</Link>
    <Header title="トップページ"/>
  </div>
);
// src/page/about.js 

import React from "react"
import { Link } from 'gatsby'
import Header from '../components/Header'

export default () => (
  <div>
    <Link to="/">top</Link>
    <Header title="aboutページ"/>
  </div>
);

f:id:kojirooooocks:20190410021632p:plain

f:id:kojirooooocks:20190410021643p:plain

propsもcssも動いてます。

本で紹介されている CSS-in-JSという方法も試してみます。

CSS-in-JSのためのライブラリインストール
$ yarn add gatsby-plugin-emotion emotion emotion-server react-emotion @emotion/core
.
..
...
....
.....
✨  Done in 18.36s.

本で紹介されているインストールでは @emotion/core をインストールするということは書いてなかったのですが、現バージョンだとエラーが出てしまったので、 @emotion/core をインストールするようにしました。

gatsby-config.jsを作成
// gatsby-config.js 

module.exports = {
  plugins: [
    `gatsby-plugin-emotion`
  ]
}
componentsを修正
// src/components/Header.js 

import React from 'react';
import { css } from 'emotion';
const style = css({
  color: 'blue'
});
export default props => <h1 className={style}>{props.title}</h1>

f:id:kojirooooocks:20190410021658p:plain

f:id:kojirooooocks:20190410021709p:plain

問題なくスタイルが適用されました!

3. GraphQLを使用してみる

プラグインをインストール
$ yarn add gatsby-source-filesystem gatsby-transformer-remark
.
..
✨  Done in 13.13s.
gatsby-config.jsを修正
// gatsby-config.js

module.exports = {
  plugins: [
    `gatsby-plugin-emotion`,
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `src`,
        path: `${__dirname}/src/`,
      },
    },
    `gatsby-transformer-remark`
  ]
}
markdownを作成
// src/md/blog.md

---
title: 'ブログタイトル'
date: '2019-04-10'
---
ブログの記事内容です。
GraphQLページで記入して確認
{
  allMarkdownRemark {
    edges {
      node {
        frontmatter {
          title
          date
        }
        excerpt
        timeToRead
        html
      }
    }
  }
}

f:id:kojirooooocks:20190410021725p:plain

先ほど入れたpluginがうまく動いてくれているようです。

  • gatsby-source-filesystem

    => ソースからデータを取得する

  • gatsby-transformer-remark

    => 読み込んだmarkdownデータを加工してGraphQLデータとして扱う

Markdownファイルのデータを使用してページに反映する
// src/pages/index.js

import React from "react"
import { graphql, Link } from 'gatsby'
import Header from '../components/Header'

export default ({data}) => (
  <div>
    <Header title="トップページ" />
    <Link to="/about/">about</Link>
    {data.allMarkdownRemark.edges.map(({node}) => (
      <div key={node.id}>
        <h3>
          {node.frontmatter.title}
          <span>
            {node.frontmatter.date}
          </span>
        </h3>
        <p>{node.excerpt}</p>
      </div>
    ))}
  </div>
);

export const query = graphql`
  query {
    allMarkdownRemark {
      totalCount
      edges {
        node {
          id
          frontmatter {
            title
            date(formatString: "DD MMMM, YYYY")
          }
          excerpt
        }
      }
    }
  }
`;

f:id:kojirooooocks:20190410021739p:plain

GraphQL初めて使いましたが、自分はあまり有効なイメージが湧きませんでした。

もうちょっと勉強します。。。

終わりに

チュートリアルとかも見たことない自分でもとてもわかり易く試しやすい記事でした。

また本書では、更に、PWAサポートや、Netlifyで公開する対応なども親切に記載されていました。

興味がある人は是非一読ください。

更に、最初に紹介させてもらった @mottox2さんのブログにもGatsbyJSについての情報が多くありますので、ぜひ一度御覧ください。

mottox2.com

簡単でしたが今回はここまでです。

積本消化月間 vol.1

はじめに

こんばんは。

溜まりに溜まっている積本を消化したいと思っていてるんですが、最近忙しくてなかなか消化できずにいました。

ただ、あらたに新たに興味が湧いた本がでてきたので、さっさと今の積本を消化しないといけなくなりました。

理由は、今年の目標で、「1ヶ月以上の積本を作らない(去年からの持ち越しは除く)」というのがあるからです。

去年から持ち越している今の積本をすべて消化しないと、新たな本を買っても積んでしまう可能性があるため、目標違反になってしまいます。。。

また、この3月の目標で「積本3冊消化」というものを掲げているので、頑張らないといけません。

というわけで、自分が頑張って消化した本を上げていきます。

読んだ本

Nuxt.jsビギナーズガイド

Nuxt.jsビギナーズガイド―Vue.js ベースのフレームワークによるシングルページアプリケーション開発

Nuxt.jsビギナーズガイド―Vue.js ベースのフレームワークによるシングルページアプリケーション開発

  • 作者:花谷 拓磨
  • 出版社/メーカー: シーアンドアール研究所
  • 発売日: 2018/10/17
  • メディア: 単行本(ソフトカバー)

PHPカンファレンスとかでも登壇されていた、花谷さんの本です。

実は去年から溜まってた本をやっと消化できました。

ページ数も多くないので、読み進もうと決めてからは早かったです。

現在実務でNuxt.jsを触りはじめているので、コンポーネント作成方法や、axiosの使用方法、テスト(jest)の使用方法などは事前知識が役に立ち、スラスラと理解できました。

実務ではelement-uiを使用する開発を行っているのですが、本書でもelement-uiが使用されていたので、なおのこと親しみやすかったです。

特に勉強になったのは、デプロイ関係の話でした。

この辺の実務に直結するような話は、やってみた系の本ではなかなか「具体的に」かかれないイメージだったのですが、各モードの運用時のメリット・デメリットから始まり、デプロイ先の候補から、さらにそのデプロイ先のメリット・デメリットもきちんと紹介されており、とてもわかり易かったです。

また、実務でも絶対やっている、サードパーティモジュールの探し方や、そもそも自分がモジュールを作る際の開発方法など、Nuxt.jsについてのチュートリアル的なところ以外の説明がとにかく豊富な印象でした。

Web+DB Press vol.108

定期購読中のwebdbです。

この本は2019/01/05 に発売なので、1ヶ月以上立っちゃっているのですが、先に紹介した目標を立てる前に届いている本なので、自分の中でノーカンにしています。。。

毎回 WebDBPressは、自分が興味あるものだけピックアップして読んで試しているので、今回も例に習ってそれだけやったりしてました。

コチラの本でピックアップして読んだのは以下です。

Web+DB Press vol.109

WebDBPressの最新号です。

こちらも同じく気になったものだけピックアップして読んでます。

  • Fastly
  • 実践Kotlin
  • 速習Puppeteer
  • 現場で役立つDB操作テクニック
  • ヘッドレスCMS

終わりに

今回読んだ2冊のWebDBPressのなかで手を動かしつつ読み進めたものが以下になります。

  • GatsbyJS
  • PHPUnit + Phake
  • 実践Kotlin
  • 速習Puppeteer
  • ヘッドレスCMS

手を動かしてやってみた↑の5つは、別でブログにあげようと思います。

簡単になりますが、とりあえず今日はここまでで。。。

laravelのjsonSchemaValidateをpublicにした

はじめに

こんばんは。

ただの紹介ブログです。

前回の記事で紹介した、JsonSchemaを使用するLaravelのリクエストレスポンスの型チェックをpackagistに公開してみました。

kojirooooocks.hatenablog.com

コチラです。

packagist.org

せっかくなので自分で書いたREADMEに沿ってやってみます。

やってみた

インストール

$ composer require kojirock5260/laravel-json-schema-validate
Using version ^1.0 for kojirock5260/laravel-json-schema-validate
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Installing kojirock5260/laravel-json-schema-validate (v1.0): Downloading (100%)         
Writing lock file
Generating optimized autoload files
> Illuminate\Foundation\ComposerScripts::postAutoloadDump
> @php artisan package:discover --ansi
Discovered Package: aws/aws-sdk-php-laravel
Discovered Package: barryvdh/laravel-cors
Discovered Package: barryvdh/laravel-ide-helper
Discovered Package: beyondcode/laravel-dump-server
Discovered Package: fideloper/proxy
Discovered Package: kojirock5260/laravel-json-schema-validate
Discovered Package: mpociot/laravel-apidoc-generator
Discovered Package: nesbot/carbon
Discovered Package: nunomaduro/collision
Discovered Package: nunomaduro/larastan
Discovered Package: sentry/sentry-laravel
Discovered Package: tymon/jwt-auth
Discovered Package: xethron/migrations-generator
Package manifest generated successfully.
ocramius/package-versions:  Generating version class...
ocramius/package-versions: ...done generating version class
——

config追加

 $ php artisan vendor:publish --provider=Kojirock\\JsonSchemaServiceProvider
Copied File [/vendor/kojirock5260/laravel-json-schema-validate/config/json-schema.php] To [/config/json-schema.php]
Publishing complete.

こちらのconfigはほぼほぼ何もありません。 middleware内で投げられる例外クラスを指定できるだけです。

<?php

declare(strict_types=1);

return [
    'exception' => Kojirock\Exception\JsonSchemaException::class
];

今回自分が作ったライブラリは justinrainbow/json-schema というライブラリに依存しています。

packagist.org

コチラの現在のlatestバージョンではエラーメッセージを指定できない仕様となっています。

このあたりとかで、直接エラーメッセージを登録しています↓

https://github.com/justinrainbow/json-schema/blob/5.2.8/src/JsonSchema/Constraints/ObjectConstraint.php#L69

開発していた際に、日本語でエラーメッセージを返したいなとなり、例外を拡張してエラーメッセージをカスタムできるように自分でもしていたので、もし誰が使うとなった場合多分その需要はありそうだなと思い、指定できるように変更しました。

ただ、issueがあがっているので、対応された場合はこの指定は必要なくなると思います。

github.com

middleware追加

app/Http/Kernel.phpを修正します。

    protected $routeMiddleware = [
        ...
        'json_schema' => \Kojirock\Middleware\JsonSchemaValidate::class,
    ];

routesに設定

<?php

Route::group(['middleware' => ['json_schema']], function () {
    Route::get('/member', 'MemberController@index')->name('MemberList');
});

schemaクラスを追加

デフォルトで App\Http\Schema\Request にRequest用、 App\Http\Schema\Response にResponse用のスキーマクラスを作ります。

<?php

declare(strict_types=1);

namespace App\Http\Schema\Request;

use Kojirock\SchemaInterface;

class MemberListSchema implements SchemaInterface
{
    public static function getSchema(): array
    {
        return [
            '$schema'    => 'http://json-schema.org/draft-07/schema#',
            'required'   => ['page'],
            'type'       => 'object',
            'properties' => [
                'page' => [
                    'type'    => 'string',
                    'pattern' => '^([1-9]|[1-9][0-9]*)$',
                ],
                'employment' => [
                    'type'    => 'string',
                    'enum'    => array_map('strval', array_keys(\App\Models\Member::EMPLOYMENT_LIST)),
                ],
                'department' => [
                    'type'    => 'string',
                    'enum'    => array_map('strval', array_keys(\App\Models\Member::DEPARTMENT_LIST)),
                ],
                'mailAddress' => [
                    'type' => 'string',
                    'format' => 'email'
                ],
            ],
        ];
    }
}

これでAPIを実行してみると、リクエスト・レスポンスの方のチェックをしてくれます。

Schemaクラスの格納場所はデフォルトで App\Http\Schema\ になりますが、APIのエンドポイントが増えるとものすごい増えていくと思います。

Schemaクラスの指定methodをオーバーライドしてあげれば、自由に格納場所を設定できます。

https://github.com/kojirock5260/laravel-json-schema-validate#schema-directory-customise

終わりに

前回の記事を書いた後気になった部分や、カスタマイズしたいなと思った部分などを盛り込んで、やってみました。

勢いだけでやったのでtestをかけてないのがダメダメな部分です...

テスト書いたらまたアップデートしようと思います...

現場からは以上です。

LaravelでJsonSchema使いたい

はじめに

こんばんは。

Laravelの開発を始めております。

APIでLaravelを使用してるのですが、Request/Responseのチェックをできないものかと考えていて、jsonSchema使えばいいじゃんと思い早速探してみました。

packagistを調べてると、 それっぽいライブラリ を発見したのですが、なんか思ってたものと違うのと、 ServiceProviderでやってたので、どちらかというと Middlewareの仕事かな?と思い、Middlewareで作ってみました。

検証環境は以下

  • php: 7.3.2
  • Laravel: 5.8.4

コード

1. JsonSchemaValidate.php(Middleware)

<?php

declare(strict_types=1);

namespace App\Http\Middleware;

use App\Exceptions\JsonSchemaException;
use Closure;
use Illuminate\Support\Facades\Route;
use JsonSchema\Validator;

class JsonSchemaValidate
{
    const TYPE_REQUEST  = 'Request';

    const TYPE_RESPONSE = 'Response';

    /**
     * Handle an incoming request.
     *
     * @param \Illuminate\Http\Request $request
     * @param \Closure                 $next
     * @throws JsonSchemaException
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $routeName =$this->getRouteName($request);

        if ($routeName === null) {
            return $next($request);
        }

        $validator = new Validator();

        $this->validateRequest($validator, $request, $routeName);

        $response = $next($request);

        $this->validateResponse($validator, $response, $routeName);

        return $response;
    }

    /**
     * @param Validator                $v
     * @param \Illuminate\Http\Request $request
     * @param string                   $routeName
     * @throws JsonSchemaException
     */
    public function validateRequest(Validator $v, \Illuminate\Http\Request $request, string $routeName): void
    {
        $requestSchema = $this->getSchema($routeName, self::TYPE_REQUEST);

        if ($requestSchema !== null) {
            $v->check((object)$request->all(), $requestSchema);

            if ($v->numErrors() >= 1) {
                throw new JsonSchemaException(serialize($v->getErrors()));
            }
        }
    }

    /**
     * @param Validator                 $v
     * @param \Illuminate\Http\JsonResponse $response
     * @param string                    $routeName
     * @throws JsonSchemaException
     */
    public function validateResponse(Validator $v, \Illuminate\Http\JsonResponse $response, string $routeName): void
    {
        $responseSchema = $this->getSchema($routeName, self::TYPE_RESPONSE);

        if ($responseSchema !== null) {
            $v->check($response->getData(), $responseSchema);

            if ($v->numErrors() >= 1) {
                throw new JsonSchemaException(serialize($v->getErrors()));
            }
        }
    }

    /**
     * @param string $routeName
     * @param string $type
     * @return null|array
     */
    public function getSchema(string $routeName, string $type): ?array
    {
        $className = $this->getJsonSchemaClassName($routeName, $type);
        return class_exists($className) ? $className::getSchema() : null;
    }

    /**
     * @param string $routeName
     * @param string $type
     * @return string
     */
    public function getJsonSchemaClassName(string $routeName, string $type): string
    {
        return "App\\Http\\Schema\\{$type}\\{$routeName}Schema";
    }

    /**
     * @param \Illuminate\Http\Request $request
     * @return null|string
     */
    public function getRouteName(\Illuminate\Http\Request $request): ?string
    {
        $route = Route::getRoutes()->match($request);
        return $route->getName();
    }
}

2.MemberListSchema.php(RequestパラメータのJsonSchema)

<?php

declare(strict_types=1);

namespace App\Http\Schema\Request;

class MemberListSchema
{
    /**
     * @return array
     */
    public static function getSchema(): array
    {
        return [
            '$schema'    => 'http://json-schema.org/draft-07/schema#',
            'required'   => ['page'],
            'type'       => 'object',
            'properties' => [
                'page' => ['type' => 'string'],
            ],
        ];
    }
}

3.MemberListSchema.php(ResponseデータのJsonSchema)

<?php

declare(strict_types=1);

namespace App\Http\Schema\Response;

class MemberListSchema
{
    /**
     * @return array
     */
    public static function getSchema(): array
    {
        return [
            '$schema'    => 'http://json-schema.org/draft-07/schema#',
            'required'   => ['total', 'data'],
            'type'       => 'object',
            'properties' => [
                'total' => ['type' => 'number'],
                'data'  => [
                    'type'  => 'array',
                    'items' => [
                        'required'   => [
                            'id',
                            'lastName',
                            'firstName',
                            'mailAddress',
                            'roles'
                        ],
                        'type'       => 'object',
                        'properties' => [
                            'id'          => ['type' => 'number'],
                            'lastName'    => ['type' => 'string'],
                            'firstName'   => ['type' => 'string'],
                            'mailAddress' => ['type' => 'string'],
                            'roles'       => ['type' => 'array'],
                        ],
                    ],
                ],
            ],
        ];
    }
}

4.api.php(routes)

<?php

declare(strict_types=1);

Route::group(['middleware' => ['json_schema']], function (): void {
    Route::get('/member', 'MemberController@list')->name('MemberList');

ルートの name() で判定しています。

なので、ルートの名前が設定されてないと何も出来ません...

例えばリクエストで、pageというパラメータを送らない場合、以下のようなレスポンスになります。

f:id:kojirooooocks:20190316013149p:plain

とりあえず動いているみたいです。

終わりに

最初はschemaをjsonファイルで作ってたのですが、毎回 file_get_contents()するのがダサいなと思ったので、schemeクラスにしました。

もしかしたら自分が調べきれてないだけで、Laravel自体にこういう機能ってあるのかな?

もし知ってたら誰か教えてください。

簡単になりますが、現場からは以上です。

追記

レスポンスの型チェックは、正常系のレスポンスのときでないと、別のExceptionが投げられた際も型チェックしてしまうことに気づきました...

レスポンス部分は以下のように変えたほうが良さそうです。

    public function validateResponse(Validator $v, \Illuminate\Http\JsonResponse $response, string $routeName): void
    {
        $responseSchema = $this->getSchema($routeName, self::TYPE_RESPONSE);

        if ($responseSchema !== null) {
            if ($response->isSuccessful()) {
                $v->check($response->getData(), $responseSchema);
                if ($v->numErrors() >= 1) {
                    throw new JsonSchemaException(serialize($v->getErrors()));
                }
            }
        }
    }

laravelのセッションを別プロジェクトに持っていく

はじめに

こんばんは。 今回も備忘録ブログです。 最近多いな。備忘録。

現在お仕事もらっている案件で、独自フレームワークを使用しているPHPプロジェクトをLaravelに載せ替えるという案件をやっています。

理由としてはよくある感じで、その独自フレームワークを保守できる人が居なくなっており、コア部分もバンバンnoticeでているみたいな状態だからのようです。

すでにそのプロジェクトのとあるディレクトリにLaravelプロジェクトが入っており、Sessionを使わないところ以外は部分的に載せ替えられているが、Sessionを使用する部分では、当然ですがLaravel側の求めるSessionの形ではないので、Sessionの共存が出来ないようでした。

今回無理やりですが、共存できるようにSessionHandlerを作成して、そのクラスを session_set_save_handler() で指定して使用するようにしました。

前提として、SessionはDBを使用しています。

やったこと

1. 旧システムにlaravelをinstall

本当は Illuminate の該当部分のみでよいのかもしれませんが、とりあえず全部入れてみました。。 この辺適当です。すいません。

$ composer require laravel/laravel

2. laravel用sessionテーブル作成

artisanのコマンドでsessionテーブルのマイグレーションファイルを作成して、実行します。

$ php artisan session:table 
$ php artisan migrate

3. laravel側session設定

config/session.php を設定します。 該当箇所は driver cookie の2つです。 実際は、.envに以下を追加するという修正です。

SESSION_DRIVER=database
SESSION_COOKIE=xxxxxxxxxxxxxxxx

4. laravel側session設定を旧プロジェクトに持っていく

config/session.php の各設定を旧プロジェクトに定数として持っていきます。 以下はlaravel5.8のデフォルト設定です。

define("SESSION_NAME", "xxxxxxxxxxxxxxxxxxxxx");
define("SESSION_TIME", time() + (120 * 60));
define("SESSION_PATH", "/");
define("SESSION_DOMAIN", null);
define("SESSION_SECURE", false);
define("SESSION_HTTP_ONLY", true);

5. application keyと、cipherの設定を旧プロジェクトにコピーして持っていく

laravelプロジェクトを作成する際に 以下のコマンドでアプリケーションのkeyを作成すると思います。

$ php artisan key:generate

こちらのkeyを旧プロジェクトにdefineやconstなどで定義しておきます。

define('LARAVEL_APP_KEY', 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz');

また、 laravelの config/app.php に設定されている cipher の値を同じく定数化しておきます。 laravel5.8のデフォルトだと AES-256-CBC になっていると思います。

define('LARAVEL_CIPHER', 'AES-256-CBC');

この2つは 暗号化されたsessionIdを作成するために必要です。

6. php.serialize_handlerをphp_serializeに変更

sessionがシリアライズされる方法をphpserialize() で実行された状態と同じにします。

ini_set('session.serialize_handler', 'php_serialize');

7. SessionHandlerを作成

<?php

namespace CompanyName\Session;

use CompanyName\Util\Util;

class AppSessionHandler implements \SessionHandlerInterface
{
    /**
     * @var string
     */
    protected $decryptSessionId;

    /**
     * @var \PDO
     */
    protected $pdo;

    /**
     * AppSessionHandler constructor.
     * @param string $decryptSessionId
     * @param \PDO $pdo
     */
    public function __construct(string $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;

        $payload   = $this->getSavePayload($sessionId, $sessionData);
        $stmt = $this->pdo->prepare($sql);
        $stmt->bindValue(":id", $this->decryptSessionId);
        $stmt->bindValue(":ip_address", Util::ip());
        $stmt->bindValue(":user_agent", Util::userAgent());
        $stmt->bindValue(":payload", base64_encode(serialize($payload)));
        $stmt->bindValue(":last_activity", Util::currentTime());
        $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;
    }

    /**
     * @param string $sessionId
     * @param string $sessionData
     * @return array
     */
    protected function getSavePayload(string $sessionId, string $sessionData):array
    {
        $registeredPayload = $this->read($sessionId);
        if (strlen($registeredPayload) >= 1) {
            $registeredPayload = unserialize($registeredPayload);
        } else {
            $registeredPayload = $this->initPayload();
        }

        if (strlen($sessionData) >= 1) {
            $payload = array_merge($registeredPayload, unserialize($sessionData));
        } else {
            $payload = $registeredPayload;
        }
        $payload['_previous'] = ['url' => Util::referer()];

        return $payload;
    }

    /**
     * payloadの初期化
     * @return array
     */
    protected function initPayload():array
    {
        return $payload = [
            '_token'    => \Illuminate\Support\Str::random(40),
            '_flash'    => ['old' => [], 'new' => []]
        ];
    }
}

対応した部分はここまでになります。

これらを設定したのが以下のコードです。

<?php
define("LARAVEL_APP_KEY", "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz");
define("SESSION_NAME", "xxxxxxxxxxxxxxxxxxxxx");
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");
ini_set('session.serialize_handler', 'php_serialize');

$encryptValue = base64_decode(substr(LARAVEL_APP_KEY, 7));
$encrypter    = new \Illuminate\Encryption\Encrypter($encryptValue, LARAVEL_CIPHER);

if (!isset($_COOKIE[SESSION_NAME])) {
    $sessionId        = \Illuminate\Support\Str::random(40);
    $encryptSessionId = $encrypter->encrypt($sessionId, false);
    setcookie(SESSION_NAME,  $encryptSessionId, SESSION_TIME,SESSION_PATH, SESSION_DOMAIN, SESSION_SECURE, LARAVEL_CIPHER);
} else {
    $decryptSessionId = $encrypter->decrypt($_COOKIE[SESSION_NAME], false);
    $pdo              = \CompanyName\Util\Db::getInstance(DBNAME, DBHOST, DBUSER, DBPASSWORD);
    $sessionHandler   = new \CompanyName\Session\AppSessionHandler($decryptSessionId, $pdo);
    session_name(SESSION_NAME);
    session_set_save_handler($sessionHandler, true);
}

これで、旧システムでSessionを使うような処理(ログインなど)を行っている状態で、laravelページに遷移後、 session()->all() などを行うと旧プロジェクトでのSession情報が取れ、その逆も可能になりました。

なんか抜けもありそうだから、もうちょい調査しますが、とりあえずここまで。

element-uiのrulesのmin, maxは文字列のrules

はじめに

こんばんは。

今回も簡単な備忘録です。

現在 element-uiを使用したwebアプリケーションを開発しています。

atomic designの設計を組み込もうとして、時間が足らず結局導入断念したのですが、分担を分けて細かく設計できるところはしていこうくらいのゆるいのりで、対応しています。

今回作業して少しだけハマったのは、 numberのみの入力を許可する AppInputNumber.vue というコンポーネントを作成したときでした。

element-uiでは、formを作成するときに、rulesというものを設定でき、そのrulesを特定のタイミングでvalidatorとして適用できます。

今回のコンポーネントは numberのみ(数値のみ)の入力しかさせたくないコンポーネントだったので、formのtypeもnumberにしていました。

最初はなんの考えもなくrules の min, maxを使って、numberの最小値、最大値を設定していたのですが、後々rulesのmin, maxは「文字数の制限」ということがわかりました...(調べてない男)

結局最小値・最大値は inputタグの min, maxでの設定になってしまいました。

今回の要件としては マイナス値を許容させてたくなかったので minが0以上になるように設定したのですが、結局コピペで通過されちゃうので、苦肉の策で rulesのパターンで乗り切りました。

コード

<template>
  <el-form-item :label="label" :prop="name" :rules="rules">
    <el-input
      type="number"
      :placeholder="placeholder"
      :disabled="disabled"
      v-model="number"
      :min="minRule"
      :max="max"
      clearable
    />
  </el-form-item>
</template>

<script>
export default {
  props: {
    model: { required: true, default: null },
    name: { type: String, required: true, default: null },
    label: { type: String, default: null },
    placeholder: { type: String, default: '数値を入力して下さい' },
    required: { type: Boolean, default: false },
    disabled: { type: Boolean, default: false },
    min: { type: Number, default: 0 },
    max: { type: Number }
  },
  data() {
    return {
      rules: [
        {
          required: this.required,
          trigger: 'blur',
          message: `${this.label}は必須項目です`
        },
        {
          pattern: /^(0|[1-9]|[1-9][0-9]+)$/,
          trigger: 'blur',
          message: `${this.label}は整数値以外は受け付けません`
        }
      ]
    }
  },
  computed: {
    number: {
      /**
       * number Getter
       * @returns {number}
       */
      get() {
        return this.model
      },
      /**
       * number Setter
       * @param {number}
       */
      set(value) {
        this.$emit('update:model', value)
      }
    },
    /**
     * 最小値ルールを取得
     * マイナス値は許容しない
     * @returns {number}
     */
    minRule() {
      return this.min < 0 ? 0 : this.min
    }
  }
}
</script>

なんかうまいやり方あったらおしえてください。

あと、element-ui見た目も綺麗で便利でいいのですが、test囲うとすると結構ハマるポイントも有りました。

それも今度あげようと思います。

終わりに

最近、家庭の事情でちょっと仕事に集中しにくい状況になってます...

落ち着くまでは、ブログ内容も更新頻度も大したことないものになってしまいそうですが、よろしくおねがいします...

phpで実装したファイルダウンロードが、chromeで変な挙動になる。

はじめに

こんばんは。

前回に続いて簡単なものです。

本日、phpで実装したファイルダウンロードがおかしくなるという話をききました。

現象としては、'donwload.csv' というように、シングルクォーテーションがついてきてしまうという現象でした。

実際に試してみると、自分が使っている最新のchromeでも確かにその現象が起きていました。

ちなみに試したのはmacchromeで、バージョン 72.0.3626.109 になります。

コード

上記の現象が起きたコードはこんな感じのコードでした。

public function download($fileSize, $contentType, $downloadName) {
    header("Content-Type: {$contentType}");
    header("Content-Length: {$fileSize}");
    header("Content-Disposition: attachment; filename='{$downloadName}'");
    readfile($filePath);
}

headerのコードのfilenameの部分をシングルクォーテーションで囲んでいる部分が問題のようでした。

該当のコードを

header("Content-Disposition: attachment; filename=\"{$downloadName}\"");

のようにダブルクォートで囲うようにするか、そもそもクォーテーションで囲わなければ同現象がおきているchromeでも問題なくdownloadできました。

chrome singlequote download とかでググると、同現象と同じ現象かな?と思える物が見つかりました。

こちらでは、とりあえずEdgeか古いバージョン使えと書いていました...

何か他に情報あれば教えてください。