20200601のPHPに関する記事は17件です。

[PHP]引数で受け取った無名関数(クロージャ)の結果の型を指定する方法

引数で受け取った無名関数の返り値が型チェックでエラーになるようにするというのが目的。

お題

phpの引数は型指定できますが、受け取った無名関数の返り値の指定はできません。
例えば下記は配列のフィルタを行うクラスの実装例です。

<?php

class ArrayObj
{
    private $values = [1, 2, 3, 4];

    public function filter(callable $filter): array
    {
        $filteredValue = [];
        foreach ($this->values as $key => $value) {
            if ($filter($value)) {
                $filteredValue[] = $value;
            }
        }
        return $filteredValue;
    }
}

$arrayObj = new ArrayObj();

// array(3) { [0]=> int(1) [1]=> int(3) [2]=> int(4) }
var_dump($arrayObj->filter(function($value) {
    return $value <> 2;
}));

// array(4) { [0]=> int(1) [1]=> int(2) [2]=> int(3) [3]=> int(4) }
var_dump($arrayObj->filter(function($value) {
    return "test";
}));

ArrayObjクラスのfilterメソッドはフィルタリングの判定用の無名関数を受け取ります。bool型を返す無名関数がほしいところですが、文字列を返す無名関数も受け取り処理できてしいます。これをbool型以外を返す無名関数が渡されたときはエラーにするようにします。

型チェック関数を利用する

単純に実装するには型チェック関数を利用するという方法があります。bool型をチェックするので`is_bool関数を使用します。

<?php

class ArrayObj
{
    private $values = [1, 2, 3, 4];

    public function getValues()
    {
        return $this->values;
    }

    public function filter(callable $filter): array
    {
        $filteredValue = [];
        foreach ($this->values as $value) {
            // 型チェック関数を追加
            if (! is_bool($filter($value))) {
                throw new \TypeError();
            }
            if ($filter($value)) {
                $filteredValue[] = $value;
            }
        }
        return $filteredValue;
    }
}

$arrayObj = new ArrayObj();

// array(3) { [0]=> int(1) [1]=> int(3) [2]=> int(4) }
var_dump($arrayObj->filter(function($value) {
    return $value <> 2;
}));

// Fatal error: Uncaught TypeError
var_dump($arrayObj->filter(function($value) {
    return "test";
}));

文字列を返す無名関数を渡すとエラーになりました。しかし

  • 型チェックがバリバリロジックに組み込まれてる
  • エラーハンドリングを自前で実装しなきゃいけない

ていう問題があります。この辺はPHPの型チェックに任せたいところ。

引数の型指定をインターフェースにする

引数の型指定を無名関数ではなく、インターフェースにしてしまうという方法。Javaでいう関数型インターフェースのような使い方。

<?php

// 厳格な型チェックを追加
declare(strict_types=1);

// フィルタリング用のインターフェース定義
interface FilterInterface
{
    function filter($value): bool;
}

class ArrayObj
{
    private $values = [1, 2, 3, 4];

    public function getValues()
    {
        return $this->values;
    }

    // フィルタリング用のインターフェースを実装したクラスを受け取るように変更
    public function filter(FilterInterface $filter): array
    {
        $filteredValue = [];
        foreach ($this->values as $value) {
            // フィルタリング用のクラスでフィルタリング
            if ($filter->filter($value)) {
                $filteredValue[] = $value;
            }
        }
        return $filteredValue;
    }
}

$arrayObj = new ArrayObj();

// array(3) { [0]=> int(1) [1]=> int(3) [2]=> int(4) }
var_dump($arrayObj->filter(new class implements FilterInterface {
    public function filter($value): bool
    {
        return $value <> 2;
    }
}));

// Fatal error:  Uncaught TypeError: Return value of class@anonymous::filter() must be of the type bool
var_dump($arrayObj->filter(new class implements FilterInterface {
    public function filter($value): bool
    {
        return "test";
    }
}));

一応、型チェックをロジックと自前のエラーハンドリングを実装せずにすみました。しかし、利用側がいちいちインターフェースを実装したクラスを実装しなければならないので面倒臭いです。Javaのラムダ式のように省略して記述できればいいのですが、、、

っていうかこれ「無名関数の返り値が型チェックでエラーになるようにする」っていう趣旨からずれまくっているような気がする。

戻り値の型宣言をした無名関数を通す

無名関数の受け取り側で、返り値の型を指定した無名関数に受け取った無名関数を実行させて、返り値の型チェックを行うという方法。ちょっと言ってることがよくわからないですが、具体的にはこんな感じ。

<?php

// 厳格な型チェックを追加
declare(strict_types=1);

class ArrayObj
{
    private $values = [1, 2, 3, 4];

    public function getValues()
    {
        return $this->values;
    }

    public function filter(callable $filter): array
    {
        // 戻り値の型を指定した無名関数
        $test = function (callable $filter, $value): bool {
            return $filter($value);
        };
        $filteredValue = [];
        foreach ($this->values as $key => $value) {
            // 戻り値の型を指定した無名関数で引数の無名関数を実行
            if ($test($filter, $value)) {
                $filteredValue[] = $value;
            }
        }
        return $filteredValue;
    }
}

$arrayObj = new ArrayObj();

// array(3) { [0]=> int(1) [1]=> int(3) [2]=> int(4) }
var_dump($arrayObj->filter(function($value) {
    return $value <> 2;
}));

// PHP Fatal error:  Uncaught TypeError: Return value of class@anonymous::filter() must be of the type bool, string returned
var_dump($arrayObj->filter(function($value) {
    return "test";
}));

これで一応利用側にも負担をかけずに実装できました。まあ、これが現実的なラインかなぁと。

一応、戻り値の型宣言ができるようになったとは言えども、関数の結果を見て判断されるので、一回は実行されてしまいますのでそのへんは注意が必要。

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

PHP7.0 × Zend Framework1でWebアプリケーションを作る

1.PHPのインストール -XAMPP

XAMPP = ザンプ

X:クロスプラットフォーム
A:Apatch
M:MariaDB/MySQL
P:PHP
P:Perl

PHPを用いたWebアプリケーションに必要なソフトウェア群を一度にインストールできるパッケージ。
公式サイトからダウンロード、インストール。
(Nextをポチポチしていれば問題ないはず)

完了したらXAMPPのコントロールパネルが表示される。ここで各ソフトウェアの起動・終了を行う。
image.png

赤いエラーが出ている場合
使用するポート番号が既に使用されている可能性。
https://norakura-jyoko.com/xampp-local-setup を参照

2.Zend Framework1を導入する

最新版は3だが業務の都合で1。

だいたいこの通りにやればできる。
http://wiki.tmd45.jp/wiki.cgi?page=XAMPP%A4%CBZend+Framework%A4%F2%C6%B3%C6%FE

image.png

Webアプリケーションを作成する

ZendFramewodkの推奨ディレクトリ構造

<project name>/
    application/
        configs/
            application.ini
        controllers/
            helpers/
        forms/
        layouts/
            filters/
            helpers/
            scripts/
        models/
        modules/
        services/
        views/
            filters/
            helpers/
            scripts/
        Bootstrap.php
    data/
        cache/
        indexes/
        locales/
        logs/
        sessions/
        uploads/
    docs/
    library/
    public/
        css/
        images/
        js/
        .htaccess
        index.php
    scripts/
        jobs/
        build/
    temp/
    tests/

https://framework.zend.com/manual/1.12/ja/project-structure.project.html

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

phpredisインストール時に、PHPの設定でひさしぶりにハマったのでメモ

PHP Warning:  PHP Startup: Unable to load dynamic library '/usr/local/src/phpredis/modules/redis.so' (tried: /usr/local/src/phpredis/modules/redis.so (/usr/local/src/phpredis/modules/redis.so: undefined symbol: php_json_decode_ex), /usr/lib64/php/modules//usr/local/src/phpredis/modules/redis.so.so (/usr/lib64/php/modules//usr/local/src/phpredis/modules/redis.so.so: cannot open shared object file: No such file or directory)) in Unknown on line 0

こんなエラーを見かけたら、ポイントは php_json_decode_ex

これは、jsonをパースするモジュールより前にロードしようとして失敗しているということ。

/etc/php.d の中に、適当なファイル(アルファベット順でjson.iniより後に読み込まれるようなファイル名)を作って extension=redis.so と書けば解決。

参考:https://stackoverflow.com/questions/41052999/php-warning-unable-to-load-dynamic-library-usr-lib64-php-modules-solr-so-und

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

symphonyを覚えようとしたら英語だらけだった

はじめに

現場が2020年5月末で終りを迎えた。コロナ影響による無業期間の始まりである。ほんとうに仕事がない期間は初めてなのでちょっとドキドキするが向こう1年ほどの生活資金はある。そしていままでの人間関係を総動員して、広告系のWeb現場にいる友達(元同僚)からphpの案件にチャレンジしてみないか、と面談オファーが来た(持つべきものは酒を酌み交わした友である)。で、使われてるフレームワークがsymphonyって言われたんだけど、、、
おおん?日本語の資料がめっちゃすくねぇー!

また英語なの!?

文句言ってるヒマはねぇや、やるしかねぇぇぇ
https://www.udemy.com/share/101WEaAkQadFdURHg=/
image.png

Symfony

インストール

terminal(ver.4.2をインストールする場合)
> composer create-project symfony/website-skeleton ./ "4.2.*"

Djangoでいうロケット画面を出せ

まず、phpってフォルダをテキトーに作って基点とする。そこに my-project を作成
image.png

my-projectを作成し、サーバーを起動
composer create-project symfony/skeleton my-project
cd my-project
php -S 127.0.0.1:8000 -t public

http://localhost:8000/
まぁ下の図がDjangoでいう、いわゆるロケット画面だな。
image.png

不足している拡張機能などがないか確認する

my-project> composer require symfony/requirements-checker

http://localhost:8000/check.php

image.png

check.phpを見てみると、どうやら
1. PHP accelerator を入れろ!
2. php.ini のキャッシュサイズを5M以上にしろ!
と書いてあるようだ

PHP accelerator

https://www.php.net/manual/ja/opcache.installation.php#opcache.installation.bundled

Windows の場合は zend_extension=C:\path\to\php_opcache.dll を使います。

php.ini(変更前)
;opcache.enable=1
;opcache.enable_cli=0
php.ini(変更後)
opcache.enable=1
opcache.enable_cli=1
php.ini(最終行に追記)
zend_extension=C:\php\ext\php_opcache.dll

キャッシュサイズを5M以上に

php.ini(変更前)
;realpath_cache_size = 4096k
php.ini(変更後)
realpath_cache_size = 5M

確認

> php -v
  PHP 7.4.6 (cli) (built: May 12 2020 11:38:54) ( ZTS Visual C++ 2017 x64 )
  Copyright (c) The PHP Group
  Zend Engine v3.4.0, Copyright (c) Zend Technologies
      with Zend OPcache v7.4.6, Copyright (c), by Zend Technologies

http://localhost:8000/check.php

image.png
image.png

twig

インストール

> composer require twig
> composer require doctrine

hello world

my-project/config/routes.yaml
image.png

名前空間について

Appと打ち込むとsrcフォルダをのぞきにいくようにcomposer.jsonは初期設定されている

composer.json
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },

127.0.0.1:8000でindexを呼ぶ

ルーティング

ドキュメントルートへのアクセスで my-project/src/Controller のなかの DefaultController に飛んで、index関数を実行せよ、ということになる

routes.yaml
index:
   path: /
   controller: App\Controller\DefaultController::index

コントローラファイルの作成

DefaultController
<?php
namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;

class DefaultController {
    public function index()
    {
        return new Response('<p>Hello!</p>');
    }
}

確認

Console
php -S 127.0.0.1:8000 -t public

image.png

確認2:127.0.0.1:8000/homeでindexを呼ぶ

ルーティング

routes.yaml(変更)
index:
   path: /home
   controller: App\Controller\DefaultController::index

ドキュメントルートのページはwelcomeページに
image.png
helloがドキュメントルートからhomeに変わった!
image.png
image.png

コントローラファイルを消す

上記がコントローラの一般的な作り方だが、レクチャーはこう言っている、いったんDefaultController.phpを消す。

maker

インストール

> composer require maker
> php bin/console make:controller DefaultController

コントローラファイルの作成

php bin/console make:controller DefaultController

おお、勝手に作られたワイ
image.png

確認:127.0.0.1:8000/defaultでindexを呼ぶ

なるほどね。これがアノテーション(注釈)式というらしい。緑色のコメントされている場所を "/default" から "/default1" とかにしてみると、たしかに url が移動する。
image.png

JSONを返したいときは

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class DefaultController extends AbstractController
{
    /**
     * @Route("/default", name="default")
     */
    public function index()
    {
        // 変更前
        // return $this->render('default/index.html.twig', [
        //     'controller_name' => 'DefaultController',
        // ]);

        // 変更後
        return $this->json(['username'=>'john.doe']);
    }
}

確認

Console
php -S 127.0.0.1:8000 -t public

ははぁ、なるほどね
image.png

引数取り込み式にもできる

<?php

namespace App\Controller;

use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

use Symfony\Component\HttpFoundation\Response; // 追加

class DefaultController extends AbstractController
{
    /**
     * @Route("/default/{name}", name="default")
     */
    public function index($name)
    {
        // 変更前
        // return $this->render('default/index.html.twig', [
        //     'controller_name' => 'DefaultController',
        // ]);

        // 変更後
        return new Response("Hello! $name");
    }
}

確認

Console
php -S 127.0.0.1:8000 -t public

ははぁ、なるほどね
image.png

リダイレクトしたい

<?php

namespace App\Controller;

use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

use Symfony\Component\HttpFoundation\Response;

class DefaultController extends AbstractController
{
    /**
     * @Route("/default/{name}", name="default")
     */
    public function index($name)
    {
        // 変更前
        // return $this->render('default/index.html.twig', [
        //     'controller_name' => 'DefaultController',
        // ]);

        // 変更後
        return $this->redirectToRoute('default2');
    }

    /**
     * @Route("/default2", name="default2")
     */
    public function index2()
    {
        return new Response('I am from default2 route!');
    }

}

確認

Console
php -S 127.0.0.1:8000 -t public

ははぁ、なるほどね
image.png

テンプレートエンジンについて

image.png

コントローラ

DefaultController.php(引数にusersの配列を追加しました)
<?php

namespace App\Controller;

use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

use Symfony\Component\HttpFoundation\Response;

class DefaultController extends AbstractController
{
    /**
     * @Route("/default", name="default")
     */
    public function index()
    {
        return $this->render('default/index.html.twig', [
            'controller_name' => 'DefaultController',
            'users' => ['Adam', 'Robert', 'John', 'Susan'] // add
        ]);
    }
}

テンプレート

index.html.twig(配列をループで回します)
{% extends 'base.html.twig' %}

{% block title %}Hello DefaultController!{% endblock %}

{% block body %}
<style>
    .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
    .example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>

<div class="example-wrapper">
    <h1>Hello {{ controller_name }}! ✅</h1>

    This friendly message is coming from:
    <ul>
        <li>Your controller at <code><a href="{{ 'D:/OneDrive/ドキュメント/Project/Php/my-project/src/Controller/DefaultController.php'|file_link(0) }}">src/Controller/DefaultController.php</a></code></li>
        <li>Your template at <code><a href="{{ 'D:/OneDrive/ドキュメント/Project/Php/my-project/templates/default/index.html.twig'|file_link(0) }}">templates/default/index.html.twig</a></code></li>
    </ul>

    {# add #}
    <ul>
        {% for user in users %}
            <li>{{ user }}</li>
        {% endfor %}
    </ul>

</div>
{% endblock %}

確認

Console
php -S 127.0.0.1:8000 -t public

ははぁ、なるほどね
image.png

モデル

> composer require orm

データベース作成

image.png

.envの設定

my-project/.env
###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db"
# For a PostgreSQL database, use: "postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=11&charset=utf8"
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml

DATABASE_URL=mysql://root:{password}@127.0.0.1:3306/symphonydb?serverVersion=8.0.19

###< doctrine/doctrine-bundle ###
コマンドを打ち込むとテーブル名は?と聞かれるのでUserと入力
> php bin/console make:entity
  Class name of the entity to create or update (e.g. VictoriousKangaroo):
  // テーブル名は?
  > User

    created: src/Entity/User.php
    created: src/Repository/UserRepository.php

    Entity generated! Now let's add some fields!
    You can always add more fields later manually or by re-running this command.

  // 列を追加するかい?
  New property name (press <return> to stop adding fields):
  > name

  // 列の型は?
  Field type (enter ? to see all types) [string]:
  > string

  // Null許容する?
  Can this field be null in the database (nullable) (yes/no) [no]:
  > no

  // (つぎの)列を追加するかい?(ここでEnter押して終わった)
  Add another property? Enter the property name (or press <return> to stop adding fields):      
  >

ふたつのファイルができた
image.png

Migration
> php bin/console doctrine:migrations:diff
> php bin/console doctrine:migrations:migrate
  Success!

コントローラ

いったんデータをテーブルにInsertしましょう
<?php

namespace App\Controller;

use App\Entity\User;

use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class DefaultController extends AbstractController
{
    /**
     * @Route("/default", name="default")
     */
    public function index()
    {
        $entityManager = $this->getDoctrine()->getManager();
        $user = new User;
        $user->setName('Adam');
        $user2 = new User;
        $user2->setName('Robert');
        $user3 = new User;
        $user3->setName('John');
        $user4 = new User;
        $user4->setName('Susan');
        $entityManager->persist($user);
        $entityManager->persist($user2);
        $entityManager->persist($user3);
        $entityManager->persist($user4);
        exit($entityManager->flush()); // いわゆるコミット(で、プログラムはここで停止)

        return $this->render('default/index.html.twig', [
            'controller_name' => 'DefaultController',
            'users' => ['Adam', 'Robert', 'John', 'Susan'] // add
        ]);
    }
}

確認

Console
php -S 127.0.0.1:8000 -t public

URLにアクセスすると?
image.png
ははぁ、なるほどね
オッケー!テーブルに入ってんじゃーん!俺は天才だー!:relaxed:
(って言わないとやってらんない)
image.png

テンプレート

index.html.twig(user.nameに変わってます)
{% extends 'base.html.twig' %}

{% block title %}Hello DefaultController!{% endblock %}

{% block body %}
<style>
    .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
    .example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>

<div class="example-wrapper">
    <h1>Hello {{ controller_name }}! </h1>

    This friendly message is coming from:
    <ul>
        <li>Your controller at <code><a href="{{ 'D:/OneDrive/ドキュメント/Project/Php/my-project/src/Controller/DefaultController.php'|file_link(0) }}">src/Controller/DefaultController.php</a></code></li>
        <li>Your template at <code><a href="{{ 'D:/OneDrive/ドキュメント/Project/Php/my-project/templates/default/index.html.twig'|file_link(0) }}">templates/default/index.html.twig</a></code></li>
    </ul>

    <ul>
        {% for user in users %}
            <li>{{ user.name }}</li> {# modify #}
        {% endfor %}
    </ul>

</div>
{% endblock %}

確認

Console
php -S 127.0.0.1:8000 -t public

ははぁ、なるほどね
image.png
image.png

ログ出力したい!

Console
composer require logger

こうして
image.png

こうじゃ
image.png

[2020-06-02T17:05:35.534551+09:00] request.INFO: Matched route "index". {"route":"index","route_parameters":{"_route":"index","_controller":"App\\Controller\\DefaultController::index"},"request_uri":"http://127.0.0.1:8000/","method":"GET"} []
[2020-06-02T17:05:35.537066+09:00] app.INFO: Gifts were randomized! [] []
[2020-06-02T17:05:35.578394+09:00] doctrine.DEBUG: SELECT t0.id AS id_1, t0.name AS name_2 FROM user t0 [] []

いろんなURLルーティング

コメントアウトに見える「@Route」の中の文字は特別な意味を持っている。DefaultControllerがナンダ?とかは特に気にしないで。ビデオのなかの題材に過ぎない。

DefaultController.php
<?php

namespace App\Controller;

use App\Entity\User;

use App\Services\GiftsService;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class DefaultController extends AbstractController
{
    /**
     * @Route("/default", name="default")
     */
    public function index(GiftsService $gifts)
    {
        $users = $this->getDoctrine()->getRepository(User::class)->findAll();

        return $this->render('default/index.html.twig', [
            'controller_name' => 'DefaultController',
            'users' => $users,
            'random_gift' => $gifts->gifts
        ]);
    }

    /**
     * @Route("/blog/{page?}", name="blog_list", requirements={"page": "\d+"})
     */
    public function index2() {
        return new Response('Optional parameters in url and requirements for parameters');
    }

    /**
     * @Route("/articles/{_locale}/{year}/{slug}/{category}",
     * defaults={"category": "computers"},
     * requirements={
     *  "_locale": "en|fr",
     *  "category": "computers|rtv",
     *  "year": "\d+"
     * })
     */
    public function index3() {
        return new Response('An advanced route example');
    }

    /**
     * @Route({"nl": "/over-ons", "en": "/about-us"}, name="abount_us")
     */
    // 国際化対応(nlはオランダ、enは英語)
    public function index4() {
        return new Response('Translated routes');
    }
}

フラッシュメッセージ

一発だけの揮発性メッセージ

DefaultController.php(index関数そのものにメッセージを仕込める)
    /**
     * @Route("/default", name="default")
     */
    public function index(GiftsService $gifts)
    {
        $users = $this->getDoctrine()->getRepository(User::class)->findAll();

        // これ!
        $this->addFlash('notice', 'Your changes were saved!');

        return $this->render('default/index.html.twig', [
            'controller_name' => 'DefaultController',
            'users' => $users,
            'random_gift' => $gifts->gifts
        ]);
    }
index.html.twig
{% extends 'base.html.twig' %}

{% block title %}Hello DefaultController!{% endblock %}

{% block body %}
<style>
    .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
    .example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>

<div class="example-wrapper">
    <h1>Hello {{ controller_name }}! </h1>

    This friendly message is coming from:
    <ul>
        <li>Your controller at <code><a href="{{ 'D:/OneDrive/ドキュメント/Project/Php/my-project/src/Controller/DefaultController.php'|file_link(0) }}">src/Controller/DefaultController.php</a></code></li>
        <li>Your template at <code><a href="{{ 'D:/OneDrive/ドキュメント/Project/Php/my-project/templates/default/index.html.twig'|file_link(0) }}">templates/default/index.html.twig</a></code></li>
    </ul>

    <ul>
        {% for user in users %}
            <li>{{ user.name }} - you won {{ random_gift[loop.index0] }}</li> {# modify #}
        {% endfor %}
    </ul>

    {# これ! #}
    {% for message in app.flashes('notice') %}
        <div class="flash-notice">
            {{ message }}
        </div>
    {% endfor %}

</div>
{% endblock %}

確認

Console
php -S 127.0.0.1:8000 -t public

ははぁ、なるほどね。addFlash~を消すと出力されないわけだ。
image.png

おまけ(複数の揮発性メッセージ)

addFlashを複数にすると配列に収納される
        $this->addFlash('notice', 'Your changes were saved!');
        $this->addFlash('warning', 'Your changes were saved!');
テンプレートでループ処理すると
    {% for csslabel, messages in app.flashes %}
        {% for message in messages %}
            <div class="flash-{{ csslabel }}">
                {{ message }}
            </div>
        {% endfor %}
    {% endfor %}

確認

Console
php -S 127.0.0.1:8000 -t public

ははぁ、なるほどね。
image.png

Cookie

DefaultController
    /**
     * @Route("/default", name="default")
     */
    public function index(GiftsService $gifts)
    {
        $users = $this->getDoctrine()->getRepository(User::class)->findAll();

        // これ!
        $cookie = new Cookie('my_cookie', 'cookie value', time() + 60); // now() + 60sec
        $res = new Response();
        $res->headers->setCookie($cookie);
        $res->send();

        return $this->render('default/index.html.twig', [
            'controller_name' => 'DefaultController',
            'users' => $users,
            'random_gift' => $gifts->gifts
        ]);
    }

確認

Console
php -S 127.0.0.1:8000 -t public

ははぁ、なるほどね。っていうかchromeでcookieも見れたんやな...
image.png

Cookieのクリア

Cookieを消すとき
        $res = new Response();
        $res->headers->clearCookie('my_cookie');
        $res->send();

セッション

「セッションIDの表示アンドSTOP」のところで処理が止まっちゃう(※exit)なので、手でコメントアウトして、コメントアウト外して、ってパチパチやるとセッションに入った値とIDが確認できる

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
    public function index(..., Request $request, SessionInterface $session)
    {
        // セッションIDの表示アンドSTOP
        exit($request->cookies->get('PHPSESSID'));

        // セッションに値を仕込んで表示
        $session->set('name', 'session value');
        if($session->has('name')){
            exit($session->get('name'));
        }

        // おまけ(セッション削除)
        $session->remove('name');
        // おまけ(セッション全消し)
        $session->clear();
    }

GET

    public function index(GiftsService $gifts, Request $request, SessionInterface $session)
    {
        // クエリストリングに page があれば page の数字を返しなければ 'default' を返す
        exit($request->query->get('page', 'default'));
    }

確認

Console
php -S 127.0.0.1:8000 -t public

ははぁ、なるほどね。
image.png
image.png

おまけ

        // アップロードされたファイル foo の取得
        $request->files->get('foo');

404

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

【Laravel】php artisan migrateエラーへの対応 : 1 PDOException::("SQLSTATE[HY000] [2002] Connection refused") /Applications/MAMP/htdocs/task_test/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70

【Laravel】php artisan migrateエラーへの対応 : 1 PDOException::("SQLSTATE[HY000] [2002] Connection refused") /Applications/MAMP/htdocs/task_test/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70

上記エラーを対応した際の内容を記録します.

目次


動作環境

OS : macOS Mojave 10.14.6
MAMP : 5.7
MySQL : 5.7.26
PHP : 7.2.31
laravel : v6.18.16

解決法

先に解決した方法を記載します.
.envファイルにDB_SOCKET=/Applications/MAMP/tmp/mysql/mysql.sockを追加します.

.env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_blog
DB_USERNAME=blog_user
DB_PASSWORD=mnhgtr54
//追加
DB_SOCKET=/Applications/MAMP/tmp/mysql/mysql.sock

キャッシュをクリアし、再度、マイグレーション

$ php artisan cache:clear #キャッシュをクリア
$ php artisan migrate #再度実行
Migration table created successfully.

こちらの記事を参考にしました.
https://teratail.com/questions/183824

調べたこと

MAMPのページを見るとUnixソケットのパスが記載されている.

image.png

またUnixソケットを利用する場合はSocketを使用する必要があるとのこと

image.png

ここでそもそもUnixソケットとは何かについて調べたところUNIXドメインソケット通信という通信プロトコルということが分かった

UNIXドメインソケット(英: UNIX domain socket)とは、単一のオペレーティングシステム内で実行されるプロセス間でデータを交換するためのデータ通信の終点.UNIXドメインソケットは、アドレス・名前空間としてファイルシステムを使用している。これらは、ファイルシステム内のinodeとしてプロセスから参照される。これは、2つのプロセスが通信するために、同じソケットを開くことができる。しかし、コミュニケーションは、完全にオペレーティングシステムのカーネル内で発生する

詳細はこちらのページが参考になります.

 エラー原因の考察

今回のエラーの件をまとめると

MacのターミナルとMAMPのMySQLは同一のマシン上にインストールされているため,
Unixドメインソケット通信を利用して接続する必要があった.

そのため,MAMPのヘルプにあるようにドメインソケットのアドレスとなる/Applications/MAMP/tmp/mysql/mysql.sockを指定することでターミナルとMySQLで通信ができるようになった

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

【PHP8.0】PHPにオブジェクト初期化子が導入される

これまで何度も塩漬けにされたり却下されたりしていたオブジェクト初期化子ですが、ついにPHP8.0で導入されることになりました。
オブジェクト初期化子が何かというとこれです。

class HOGE{
    public function __construct(
        private int $x
    ){
        // $HOGE->xが生える
    }
}

これはオブジェクト初期化子でいいのか?

日本語で何と表すのか適切な単語が思いつかなかったのでとりあえずオブジェクト初期化子としておきます。
愚直に訳すと"コンストラクタ引数昇格"ですが、そんな単語は無いうえに型昇格と紛らわしいです。
引数プロパティ宣言パラメータプロパティ宣言もほぼ使われてないし何と表現すればいいのだろう。
きっと誰かが適切な語をプルリクしてくれるはず。

以下は該当のRFC、PHP RFC: Constructor Property Promotionの日本語訳です。

PHP RFC: Constructor Property Promotion

Introduction

PHPでは現在のところ、オブジェクトにプロパティを定義するだけでも同じことを複数回書かなければならないため、多くの無駄が必要です。
以下の単純なクラスを考えてみましょう。

class Point {
    public float $x;
    public float $y;
    public float $z;

    public function __construct(
        float $x = 0.0,
        float $y = 0.0,
        float $z = 0.0,
    ) {
        $this->x = $x;
        $this->y = $y;
        $this->z = $z;
    }
}

プロパティの表記は、1:プロパティの宣言、2:コンストラクタの引数、3:プロパティの代入で3回も繰り返されます。
さらにプロパティの型も2箇所に書かなければなりません。

プロパティ宣言とコンストラクタ以外には何も含まれていないバリューオブジェクトでは特に、多くの重複によって変更が複雑になり、エラーを起こしやすいものとなります。

このRFCでは、プロパティの定義とコンストラクタを組み合わせるショートハンド構文の導入を提案します。

PHP8
class Point {
    public function __construct(
        public float $x = 0.0,
        public float $y = 0.0,
        public float $z = 0.0,
    ) {}
}

このショートハンド構文は、前述の例と厳密に同じで、より短く書くことができます。
構文は姉妹言語Hackから採用しています。

Proposal

コンストラクタの引数にpublic/protected/private何れかが記述されている場合、その引数は"promoteされた引数"とします。
promoteされた引数には、同じ名前のプロパティが追加され、値が割り当てられます。

Constraints

promoteはabstractではないクラスのコンストラクタでのみ記述可能です。
従って、以下のような構文は使用不能です。

// エラー:コンストラクタではない
function test(private $x) {}

abstract class Test {
    // エラー:abstractなので駄目
    abstract public function __construct(private $x);
}

interface Test {
    // エラー:interfaceも駄目
    public function __construct(private $x);
}

一般的でない使い方ですが、トレイトでは使用可能です。

対応する可視性キーワードはpublic/protected/privateのみです。

class Test {
    // エラー:varはサポートしてない
    public function __construct(var $prop) {}
}

promoteされた引数によるプロパティは、通常のプロパティと全く同じ扱いになります。
特に注意点として、同じプロパティを二度宣言することはできません。

class Test {
    public $prop;

    // Error: Redeclaration of property.
    public function __construct(public $prop) {}
}

また、プロパティにすることのできないcallable型は使用することができません。

class Test {
    // Error: Callable type not supported for properties.
    public function __construct(public callable $callback) {}
}

promoteされたプロパティはプロパティ宣言と同義であるため、デフォルトがNULLの場合はNULL許容型を明示しなければなりません。

class Test {
    // Error: Using null default on non-nullable property
    public function __construct(public Type $prop = null) {}

    // こっちはOK
    public function __construct(public ?Type $prop = null) {}
}

可変長引数をpromoteすることはできません。

class Test {
    // エラー
    public function __construct(public string ...$strings) {}
}

理由としては、明示する引数の型(ここではstring)と、実際に渡される引数の型(ここではstringの配列)が異なるからです。
$stringsプロパティをstringの配列にすることも可能ですが、それではわかりづらくなります。

promoteプロパティと明示的なプロパティ宣言を組み合わせることは可能です。
またpromoteプロパティとpromoteされない引数を同時に渡すことも可能です。

// 正しい
class Test {
    public string $explicitProp;

    public function __construct(public int $promotedProp, int $normalArg) {
        $this->explicitProp = (string) $normalArg;
    }
}

Desugaring

promoteプロパティはただのシンタックスシュガーであり、全てのpromoteプロパティに対して以下の変換が適用されます。

// シンタックスシュガー
class Test {
    public function __construct(public Type $prop = DEFAULT) {}
}

// こう展開される
class Test {
    public Type $prop;

    public function __construct(Type $prop = DEFAULT) {
        $this->prop = $prop;
    }
}

自動的に宣言されるプロパティの可視性と型は、promoteプロパティの可視性および型と同じになります。
注目すべき点は、プロパティにデフォルト値は適用されず(つまり、未初期化で始まります)、コンストラクタ引数でのみ指定されるところです。

プロパティ宣言時にもデフォルト値を指定したほうがよいようにも思えますが、将来的にデフォルト値で指定することが望ましくなるであろう理由が存在します。

ひとつめは、プロパティのデフォルト値に任意の式を利用できるようにする拡張の可能性です。

// FROM
class Test {
    public function __construct(public Dependency $prop = new Dependency()) {}
}

// TO
class Test {
    public Dependency $prop /* = new Dependency() */;

    public function __construct(Dependency $prop = new Dependency()) {
        $this->prop = $prop;
    }
}

こうなると、プロパティ宣言時とデフォルト値でオブジェクトを2回構築することとなるため望ましくありません。

また、新潟アクセス修正子のルールではプロパティでデフォルト値を宣言すると、コンストラクタで代入することもできなくなります。

promote引数が参照であった場合、プロパティも参照になります。

// FROM
class Test {
    public function __construct(public array &$array) {}
}

// TO
class Test {
    public array $array;

    public function __construct(array &$array) {
        $this->array =& $array;
    }
}

promoteプロパティへの引数の割り当ては、コンストラクタの冒頭で行われます。
従って、コンストラクタ内でも引数とプロパティの両方にアクセスすることが可能です。

// 動作する
class PositivePoint {
    public function __construct(public float $x, public float $y) {
        assert($x >= 0.0);
        assert($y >= 0.0);
    }
}

// こっちも動作する
class PositivePoint {
    public function __construct(public float $x, public float $y) {
        assert($this->x >= 0.0);
        assert($this->y >= 0.0);
    }
}

Reflection

リフレクションおよびその他の解析機構で見ると、シンタックスシュガーを解除した後の状態になります。
すなわち、promoteプロパティは明示的に宣言されたプロパティのように見え、promote引数は通常のコンストラクタ引数のように見える、ということです。

PHPは引数に関するDocコメントを公開していませんが、promoteプロパティのDocコメントも保持されます。

class Test {
    public function __construct(
        /** @SomeAnnotation() */
        public $annotatedProperty
    ) {}
}

$rp = new ReflectionProperty(Test::class, 'annotatedProperty');
echo $rp->getDocComment(); // "/** @SomeAnnotation */"

この例のように、promoteプロパティではDocコメントベースのアノテーションを使用することができます。

また、2メソッドが追加されます。

ReflectionProperty::isPromoted()は、promoteプロパティであればtrueを返します。
ReflectionParameter::isPromoted()は、promote引数であればtrueを返します。

プロパティがpromoteされたかどうかを気にする場面はほとんど存在しないと思われますが、この情報によって元のコードをより簡単に再構築することができます。

Inheritance

オブジェクト初期化子は継承することができますが、特に特筆すべきようなことはありません。
abstrautを含む典型的な継承のユースケースを以下に示します。

abstract class Node {
    public function __construct(
        protected Location $startLoc = null,
        protected Location $endLoc = null,
    ) {}
}

class ParamNode extends Node {
    public function __construct(
        public string $name,
        public ExprNode $default = null,
        public TypeNode $type = null,
        public bool $byRef = false,
        public bool $variadic = false,
        Location $startLoc = null,
        Location $endLoc = null,
    ) {
        parent::__construct($startLoc, $endLoc);
    }
}

ParamNodeクラスでいくつかのpromoteプロパティを宣言し、さらに二つの普通の引数を親コンストラクタに転送しています。
これは以下のように展開されます。

abstract class Node {
    protected Location $startLoc;
    protected Location $endLoc;

    public function __construct(
        Location $startLoc = null,
        Location $endLoc = null,
    ) {
        $this->startLoc = $startLoc;
        $this->endLoc = $endLoc;
    }
}

class ParamNode extends Node {
    public string $name;
    public ExprNode $default;
    public TypeNode $type;
    public bool $byRef;
    public bool $variadic;

    public function __construct(
        string $name,
        ExprNode $default = null,
        TypeNode $type = null,
        bool $byRef = false,
        bool $variadic = false,
        Location $startLoc = null,
        Location $endLoc = null,
    ) {
        $this->name = $name;
        $this->default = $default;
        $this->type = $type;
        $this->byRef = $byRef;
        $this->variadic = $variadic;
        parent::__construct($startLoc, $endLoc);
    }
}

プロパティへの代入は、親コンストラクタが呼ばれる前に行われることに注意してください。
これはコーディングスタイルとして一般的ではありませんが、動作に影響が出るようなことはほぼありません。

Attributes

PHP8ではアトリビュートも導入されるため、相互作用を考慮する必要があります。
アトリビュートは、プロパティと引数の両方で使用することができます。

class Test {
    public function __construct(
        <<ExampleAttribute>>
        public int $prop,
    ) {}
}

このコードがどのように解釈されるか決める必要があります。
1. アトリビュートは引数にのみ適用する。
2. アトリビュートはプロパティにのみ適用する。
3. アトリビュートは引数とプロパティの両方に適用する。
4. 曖昧さを避けるためエラーにする

// Option 1: アトリビュートは引数にのみ適用する
class Test {
    public int $prop;

    public function __construct(
        <<ExampleAttribute>>
        int $prop,
    ) {}
}

// Option 2: アトリビュートはプロパティにのみ適用する
class Test {
    <<ExampleAttribute>>
    public int $prop;

    public function __construct(
        int $prop,
    ) {}
}

// Option 3: アトリビュートは引数とプロパティの両方に適用する
class Test {
    <<ExampleAttribute>>
    public int $prop;

    public function __construct(
        <<ExampleAttribute>>
        int $prop,
    ) {}
}

// Option 4: 曖昧さを避けるためエラーにする

このRFCでは3番目、つまり引数とプロパティの両方に適用することを提案しています。
これが最も柔軟性の高い方法だからです。

ただし、これは実装に依ると考えています。
PHP8の実装に関わる作業で、アトリビュートをプロパティにのみ配置した方がよいと判明した場合は、そのように変更される場合があります。

Coding Style Consideration

このセクションではコーディングスタイルの推奨について解説します。
規程ではありません。

promoteプロパティを使用する場合、コンストラクタをクラス最初のメソッドとして、明示的なプロパティ宣言の直後に配置することをお勧めします。
これにより、全ての全てのプロパティが先頭にまとめられ、一目でわかるようになります。
静的メソッドを最初に配置することを要求しているコーディング規約は、コンストラクタを最初に配置するよう規約を調整する必要があります。

promoteプロパティに@paramアノテーションを使用している場合、ドキュメントツールは@varアノテーションも含まれているものとして解釈されるべきです。

// 元のコード
class Point {
    /**
     * Create a 3D point.
     *
     * @param float $x The X coordinate.
     * @param float $y The Y coordinate.
     * @param float $z The Z coordinate.
     */
    public function __construct(
        public float $x = 0.0,
        public float $y = 0.0,
        public float $z = 0.0,
    ) {}
}

// こう解釈する
class Point {
    /**
     * @var float $x The X coordinate.
     */
    public float $x;

    /**
     * @var float $y The Y coordinate.
     */
    public float $y;

    /**
     * @var float $z The Z coordinate.
     */
    public float $z;

    /**
     * Create a 3D point.
     *
     * @param float $x The X coordinate.
     * @param float $y The Y coordinate.
     * @param float $z The Z coordinate.
     */
    public function __construct(
        float $x = 0.0,
        float $y = 0.0,
        float $z = 0.0,
    ) {
        $this->x = $x;
        $this->y = $y;
        $this->z = $z;
    }
}

最後に、promoteプロパティは、あくまで一般的なケースをカバーするための便利な省略記法であるに過ぎないことに注意してください。
promoteプロパティはいつでも明示的なプロパティに書き換えることができます。
そのため、この変更は下位互換性を壊すことはありません。

Backward Incompatible Changes

下位互換性のない変更はありません。

Future Scope

Larryが、この機能と他の機能を組み合わせることによってオブジェクト初期化を改善する方法について、より深いビジョンを提供しています

Prior Art

この機能、あるいは類似した機能は多くの言語でサポートされています。
Hack
TypeScript
Kotlin

先行するRFCが存在します。
Automatic property initialization プロパティ宣言は必要とする、より弱い形です。
Constructor Argument Promotion このRFCとほとんど同じです。
Code free constructor Kotlinの文法に基づいています。

Vote

投票期間は2020/05/29まで、2/3+1の賛成が必要です。
このRFCは賛成46反対10で受理されました。

感想

プロパティを書くのが格段に楽になりますね。
後から見るときにプロパティが宣言されているのかどうかちょっとわかりにくそうですが、この機能が使えるのはコンストラクタだけなのでそこだけ抑えていれば大丈夫でしょう。
コンストラクタだけではなく任意のメソッドで使えると便利では、と一瞬思ったものの、これを無制限に使えると完全に収拾が付かなくなってしまうので、やはりコンストラクタだけに留めておくのが賢明そうですね。

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

Laravelのタスクスケジューラを使う

前提条件

eclipseでLaravel開発環境を構築する。デバッグでブレークポイントをつけて止める。(WindowsもVagrantもdockerも)
本記事は上記が完了している前提で書かれています
プロジェクトの作成もapacheの設定も上記で行っています

Laravelでコマンドライン処理を行う
本記事は上記の内容を理解している前提で書かれています

Commandクラス作成

コマンドラインで
cd sample
php artisan make:command SampleSchedule
xdebugの設定をしているとeclipseが実行していいですかというプロンプトを出すのでOKを押します
eclipseプロジェクトを右クリック→リフレッシュ
/sample/app/Console/Commands/SampleSchedule.phpが現れます

Commandクラス修正

さきほど作成したSampleSchedule.phpを下記に修正します

SampleSchedule.php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class SampleSchedule extends Command
{
    protected $signature = 'sample:name2 {arg1}';

    protected $description = 'SampleSchedule';

    public function __construct()
    {
        parent::__construct();
    }

    public function handle()
    {
        $arg1 = $this->argument('arg1');
        $this->line('arg1:' . $arg1);
        if ($arg1 === 'a') {
            return 1;
        }
        return 0;
    }
}

Commandクラス登録

/sample/app/Console/Kernel.php修正

Kernel.php
‥‥
    protected $commands = [
     ‥‥
        ,Commands\SampleSchedule::class
    ];

‥‥

Kernel.phpに定義されている$commands配列に先ほど作成したSampleScheduleクラス名を追記します

スケジュール作成

(1) /sample/app/Console/Kernel.phpに下記use文を追記
use Illuminate\Support\Facades\Log;

(2) /sample/app/Console/Kernel.phpのscheduleメソッドを修正します

Kernel.php
    protected function schedule(Schedule $schedule)
    {
        $schedule->command('sample:name2 0')
                 ->everyMinute()
                 ->appendOutputTo(dirname(dirname(dirname(__FILE__))) . '/storage/logs/SampleSchedule.log')
                 ->onSuccess(function () {
                     Log::info('成功');
                 })
                 ->onFailure(function () {
                     Log::error('エラー');
                 })
                 ;

        $schedule->command('sample:name2 a')
                 ->everyMinute()
                 ->appendOutputTo(dirname(dirname(dirname(__FILE__))) . '/storage/logs/SampleSchedule.log')
                 ->onSuccess(function () {
                     Log::info('成功');
                 })
                 ->onFailure(function () {
                     Log::error('エラー');
                 });

        $schedule->command('sample:name2 z')
                 ->dailyAt('10:00')
                 ->appendOutputTo(dirname(dirname(dirname(__FILE__))) . '/storage/logs/SampleSchedule.log')
                 ->onSuccess(function () {
                     Log::info('成功');
                 })
                 ->onFailure(function () {
                     Log::error('エラー');
                 })
                 ;

    }

command関数は実行するコマンドクラスを指定します。引数はCommandクラスのsignature変数に定義した通りに書きます。php artisanコマンドでCommandクラスを実行するときと同じものになります[Laravelでコマンドライン処理を行う]。
everyMinute関数は毎分実行する指定になります
appendOutputTo関数はCommandクラスからの出力先を指定したファイルにするものです
onSuccess関数はCommandクラスから戻り値が0の場合に実行される処理を指定します。ただし、動作確認したLaravelバージョン7.1.1ではCommandクラスをバックグラウンド実行すると戻り値が0でもonSuccessが実行されないです。バックグラウンド実行はメソッドチェーンでrunInBackground関数を指定するとできるようになります。
onFailure関数はCommandクラスから戻り値が1の場合に実行される処理を指定します。ただし、動作確認したLaravelバージョン7.1.1ではCommandクラスをバックグラウンド実行すると戻り値が何であってもonFailureが実行されます。バックグラウンド実行はメソッドチェーンでrunInBackground関数を指定するとできるようになります。
dailyAt関数は毎日決まった時間にCommandクラスを実行する指定になります
everyMinute関数やdailyAt関数といった実行日時を指定する関数はいろいろ用意されています。下記で確認できます
Laravel 7.x タスクスケジュール 繰り返しのスケジュールオプション

動作確認

コマンドラインで
cd sample
php artisan schedule:run
xdebugの設定をしているとeclipseが実行していいですかというプロンプトを出すのでOKを押します

/sample/storage/logs/SampleSchedule.logにSampleSchedule.phpからの出力が書き込まれています
/sample/storage/logs/laravel.logに/sample/app/Console/Kernel.phpのscheduleメソッド内にonSuccess、onFailureで指定したクロージャからの出力が書き込まれています

また、php artisan schedule:runを実行したウインドウを見ると
php "artisan" sample:name2 0
php "artisan" sample:name2 a
が実行されたことがわかります
さらに、実行した時刻が10:00の場合、php "artisan" sample:name2 zも実行されているでしょう

cron登録とスケジューラとしての使い方

先ほどコマンドラインで/sample/app/Console/Kernel.phpのscheduleメソッドを実行してみました
これをcronに指定します
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
/path-to-your-projectというのはご自身のLaravelプロジェクトへの絶対パスに書き換えてください
この記事であればsampleフォルダになります
このcronの設定を見ると分、時、日、月、曜すべてアスタリスクになっていることがわかります
これは1分に1回、要は毎分php artisan schedule:run実行されるという設定になります
つまり、毎分php artisan schedule:runが実行されて、その際、実行日時に合うタスクスケジュール繰り返しのスケジュールオプションが設定されているcommandだけが実行されるという仕組みです

先ほどコマンドラインでphp artisan schedule:runを実行した際、everyMinute指定したものが実行されて(ちょうど10:00に実行しなければ)dailyAt指定したものは実行されなかったと思います
これはphp artisan schedule:runが毎分実行されるようにつくられているからです

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

【Laravel + GraphQL】Lighthouseを使ってみる【その1:チュートリアル】

Lighthouseの設計思想が何もわからん&そもそもディレクティブ全然わからん状態を脱却したいので、練習として公式のドキュメントを上から順番にやっていくことにしました。

環境

PHP: 7.2.5
Laravel: 7.0
Lighthouse: 4.13

チュートリアル

デフォルトのスキーマを実行できるようにする

何はともかくインストールします。

composer require nuwave/lighthouse

そうしたらまずはデフォルトのスキーマを試してみましょう。

php artisan vendor:publish --provider="Nuwave\Lighthouse\LighthouseServiceProvider" --tag=schema

このコマンドを実行すると以下のようにusersuserスキーマが生成されます。

src/graphql/schema.graphql
"A date string with format `Y-m-d`, e.g. `2011-05-23`."
scalar Date @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\Date")

"A datetime string with format `Y-m-d H:i:s`, e.g. `2018-05-23 13:43:32`."
scalar DateTime @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTime")

"A datetime and timezone string in ISO 8601 format `Y-m-dTH:i:sO`, e.g. `2020-04-20T13:53:12+02:00`."
scalar DateTimeTz @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTimeTz")

type Query {
    users: [User!]! @paginate(defaultCount: 10)
    user(id: ID @eq): User @find
}

type User {
    id: ID!
    name: String!
    email: String!
    created_at: DateTime!
    updated_at: DateTime!
}

スキーマが生成されたので適当にusersテーブルにデータを入れて以下のようにcurlを叩いてみましょう。

curl --request POST \
  --url http://localhost:10080/graphql \
  --header 'content-type: application/json' \
  --data '{"query":"query {\n  users {\n    paginatorInfo {\n      count\n      currentPage\n    }\n    data {\n      id\n      name\n      email\n      created_at\n      updated_at\n    }\n  }\n}"}'

するとたったこれだけでデータベースの情報を取得することができました。
しかもペジネータも自動でついてます。

{
  "data": {
    "users": {
      "paginatorInfo": {
        "count": 1,
        "currentPage": 1
      },
      "data": [
        {
          "id": "1",
          "name": "hgoe",
          "email": "hoge@example.com",
          "created_at": "2020-05-31 14:04:07",
          "updated_at": "2020-05-31 14:04:11"
        }
      ]
    }
  }
}

CORSの有効化

CORSを有効化するのを忘れないようにしましょう。

config/cors.php
return [
-   'paths' => ['api/*'],
+   'paths' => ['api/*', 'graphql'],
    'allowed_methods' => ['*'],
    'allowed_origins' => explode(',', env('ALLOWED_CORS_ORIGINS', [])),
    ...
];

開発用ツール

Lighthouseを便利に開発するために、公式ではGraphQL Playgroundの使用を推奨しています。

しかしこのツール、Dockerを使っているとめっちゃメモリ食ってまともに動かなくなることが稀によくあります。

なので個人的にはGraphQLをサポートしているInsomniaAltair GraphQL Clientがおすすめです。

特にInsomniaは無料にも関わらず多彩な機能が搭載されており、とても便利なので現状使用している Rest Clientに不満がある方は是非とも触ってみることをおすすめします。

【RESTクライアント】Insomniaをおすすめする記事

IDEサポート

Lighthouseでは独自定義されたディレクティブが多用されているようです。
なのでこれらをIntelliJ(PhpStrom)やVS Codeでも認識できるようにするために、IDEヘルパーを導入します。

composer require --dev haydenpierce/class-finder
php artisan lighthouse:ide-helper

これによってschema-directives.graphql_lighthouse_ide_helper.phpが生成されるので、それぞれ.gitignoreに入れておきましょう。

ディレクティブ

ここからはLighthouseの主機能となるディレクティブについて説明していきます。

基本的にLaravelのModelとGraphQLのオブジェクト(type)は一対一で自動でマッピングされます。
またtypeを定義する際は主キーに対応するフィールドにはidという命名をすることが推奨されています。

type User {
  id: ID!
  name: String!
  email: String!
}

@all

@allディレクティブはEloquentのall()と同じ役割をします。

type Query {
  users: [User!]! @all
}

以下のようにスキーマを叩くと、以下のようにUserモデルに紐付けられているテーブル(今回はLaravelデフォルトのusersテーブル)の全ての情報が返ってきます。

query {
  user_all {
    id
    name
    email
  }
}
{
  "data": {
    "users": [
      {"id": "1", "name": "hoge", "email": "hoge@example.com"},
      {"id": "2", "name": "fuga", "email": "fuga@example.com"}
    ]
  }
}

@paginate

paginateディレクティブは名前の通り、ペジネーションの役割を果たします。

type Query {
    users: [User!]! @paginate
}

例えば上記のpaginateディレクティブを付与したスキーマは内部的には自動で次のように変換されます。

type Query {
  users(first: Int!, page: Int): PostPaginator
}

type PostPaginator {
  data: [User!]!
  paginatorInfo: PaginatorInfo!
}

そして以下のように叩くことができるようになります。

{
  users(first: 10) {
    data {
      id
      name
    }
    paginatorInfo {
      currentPage
      lastPage
    }
  }
}

またpaginatorには他にもいくつかの機能があります。

デフォルト数の指定

一度のリクエストで返すデフォルトのアイテム数をクエリ時にcount引数で指定することなく、デフォルトで指定できるようになります。

type Query {
    users: [User!]! @paginate(defaultCount: 10)
}

最大数の制限

ページ分割する際に要求できるアイテムの最大数を制限することができます。

type Query {
    users: [User!]! @paginate(maxCount: 10)
}

対象モデルの上書き

デフォルトではスキーマ定義で指定されたtypeと同じ名前のEloquentモデルを検索しますが、model引数を指定することでそれを上書きすることができます。

type Query {
    users: [User!]! @paginate(model: "App\\Models\\ActiveUser")
}

@find

findディレクションは渡された引数からモデル内を検索します。
Eloquentでいうwhereとfindですね。

type Query {
    user(id: ID @eq): User @find
}

注意点として複数の結果が得られた場合例外が投げられるので、確実ではない場合は@firstの使用が推奨されています。

またこちらもpaginateと同様にmodelの上書きをすることが可能です。

type Query {
    user(id: ID @eq): User @find(model: "App\\Models\\ActiveUser")
}

@eq

Eloquentクエリに等号演算子を配置するディレクティブです。

以下の例だと引数のidの値がusersテーブルのidと一致するものを検索します。

type Query {
    user(id: ID @eq): User @find
}

また引数の名前がデータベースのカラム名と異なる場合は実際の列名をkeyに渡します。

type Query {
    user(id: ID @eq(key: "user_id")): User @find
}

@create

新規のデータを登録するにはcreateディレクティブを使用することができます。

type Mutation {
    createUser(name: String!, email: String!, password: String!): User! @create
}

Mutation typeの中でスキーマを作成し、その引数に保存したいデータを指定するだけでデータベースに保存することができます。

mutation {
  createUser(
    name: "hogehoge"
    email: "hogehoge@example.com"
    password: "hogehoge"
  ) {
    id
    name
    email
    created_at
    updated_at
  }
}

ただし注意点として、作成や更新を許可するカラムに対してModelにfillableを指定する必要があります。(updateディレクティブに関しても同様)

User.php
class User extends Authenticatable
{
...
    protected $fillable = [
        'name', 'email', 'password',
    ];
}

またcreateディレクティブでも使用するmodelを変更することが可能です。

type Mutation {
    createUser(input: CreateUserInput! @spread): User! @create(model: "App\\Models\\ActiveUser")
}

独自のリクエスト型を定義する

特定のリクエスト型の定義はinputを指定することで可能です。

引数として単一のオブジェクトを使用する場合はリゾルバーに適応する前にネストした値を展開するように@spreadを使用する必要があります。

type Mutation {
    createUser(input: CreateUserInput! @spread): User! @create
}

input CreateUserInput {
    name: String!
    email: String!
    password: String!
}

参考:Lighthouse#Arg Resolvers

@update

データの更新にはupdateディレクティブを使用します。
idで指定されたデータに対して第二引数以降の値で更新します。

type Mutation {
    updateUser(id: ID!, name: String, email: String): User @update
}

ちなみに動作的には引数を指定しないと更新されず、nullや空文字を送信するとそれぞれの値で上書きされます。
上書きされたくない場合はnot nullやバリデーションを設定するようにしましょう。

またGraphQLでは一部のデータのみを更新するかをクライアント側が選択できるので、サーバ側ではオプションの引数を除く全ての引数を指定することが推奨されています。

もちろんupdateディレクティブでも独自のリクエスト型を定義して使用することが可能です。

type Mutation {
    updateUser(id: ID!, input: UpdateUserInput @spread): User @update
}

input UpdateUserInput {
    name: String
    email: String
    password: String
}

@upsert

upsertはidで指定されたデータが存在すればupdateし、なければ指定されたidで新規にデータを作成します。
またidが指定されていない場合は自動生成されたIDを使用してデータを作成します。

type Mutation {
    upsertUser(id: ID!, name: String!, email: String!, password: String!): User @upsert
}

この時DB上でnot nullなカラムをGraphQLのスキーマ上でnullableにしても新規作成時にnullの値を入れてリクエストできてしまいますが、エラーになるので注意しましょう。(updateは問題なく動きます。)

@delete

deleteはidを指定するだけで簡単にデータを削除できる、ある意味危険なディレクティブです。

type Mutation {
    deleteUser(id: ID!): User @delete
}

返り値は削除したデータがあればそれを返し、指定したidが対象となるデータがない場合はnullを返します。

複数削除

複数データを一度に削除したい場合はIDをリストにします。

type Mutation {
    deleteUser(id: [ID!]!): [User!]! @delete
}

まとめ

今回は基本的なCRUD操作に必要なディレクティブについて学習しました。
自分でスキーマを定義していると返り値や引数周りの設計で悩むことが結構多かったのですが、そこらへんがサンプルで明示的に示されていてとても勉強になりました。

ここまでやった感想としては複雑なビジネスロジックなどが必要のない簡単なアプリならSQLやModelなどを意識する必要がほとんどなく、ほとんどサーバの技術を触ったことがない人でもさっくり作れそうな印象を受けました。

流石に複雑なロジックを入れようとするとディレクティブだけでは難しいのでResolverを使っていくことになりますが、それでもペジネータなどが準備されているのは嬉しいですね。
(キャッシュ周りに関して把握してないので、どのようにデータを持つのかは気になりますが…)

次回はResolverやリレーションなどについて調べていきたいと思います。

参考文献

Lighthouse公式ドキュメント

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

Google Photos APIをPHPで使う

Google Photos APIを使用して、Google Photoにアクセスするアプリを作成しています。
その過程でのAPIを利用するまでを記載します。

基本的にオフィシャルのリファレンスを参照していますが、そこから読み取りにくい部分などを具体的に紹介できたらと思っております。

Google Photos API

Laravelを使用したコードを記載していますが、Laravelに特化した部分は少ないと思いますので、
Laravel以外のフレームワークを使っている場合もご参考になればと思います。

準備

Laravelプロジェクト作成

$ composer create-project laravel/laravel googlephoto_sample --prefer-dist

Google Photos APIを有効にする

  • Google APIsのダッシュボードからプロジェクト作成 → APIとサービスを有効化 → Google Photos APIを選択
  • Google APIsのプロジェクトが作成済みなら Google Photos API このページから直接もできます

クライアントID作成

  • Google APIsのダッシュボードから「認証情報」を選択 → 認証情報を作成 → OAuthクライアントID
  • アプリケーションの種類は、ウェブアプリケーション
  • 承認済みのリダイレクトURIは、今回は取り合えず http://localhost/photo/redirect
  • 作成したクライアントIDの画面からJSONをダウンロード → credential.jsonにリネームしておく
  • ダウンロードしたcredential.jsonをLaravelプロジェクトのフォルダに入れておきます。

Google APIs Client, Google Photos API ライブラリのインストール

composer.json
    "require": {
        "google/apiclient": "^2.0",
        "google/photos-library": "^1.5"
    },
$ composer update

composer.jsonを編集せずに、直接コマンドラインかrcomposer require google/apiclient:"^2.0"みたいにやるとなぜか失敗します。

Google Photos APIを使ってみる

PhotoControllerという名前のコントローラーと、コントローラーから呼び出されるGooglePhotoServiceという名前のクラスを作成していきます。

認証URLの生成、表示

app\Services\GooglePhotoService.php
<?php
namespace App\Services;

use Google\Auth\Credentials\UserRefreshCredentials;
use Google\Photos\Library\V1\PhotosLibraryClient;

class GooglePhotoService
{
    private function createGoogleClient()
    {
        $client = new \Google_Client();
        $client->setApplicationName('GooglePhotoSample');
        $client->setScopes(\Google_Service_PhotosLibrary::PHOTOSLIBRARY_READONLY); 
        $client->setAuthConfig(base_path("credentials.json"));
        $client->setAccessType('offline');
        return $client;
    }

    public function createAuthUrl($redirectUri)
    {
        $client = $this->createGoogleClient();
        $client->setRedirectUri($redirectUri);
        return $client->createAuthUrl();
    }
}
app\Http\Controllers\PhotoController.php
<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Services\GooglePhotoService;

class PhotoController extends Controller
{
    public function start()
    {
        $service = new GooglePhotoService();
        $url = $service->createAuthUrl("http://localhost/photo/redirect");
        return view(start, [
            'url' => $url
        ]);
    }
}

resources\views\start.blade.php
<html>
<body>
  <div class="flex-center position-ref full-height">
    <div class="content">
      Step.1<br/>
      <a href="{{$url}}">{{$url}}</a>
    </div>
  </div>
</body>
</html>
routes\web.php
Route::get('/photo/start, 'PhotoController@start);

OAuthのリダイレクト先を作成

リダイレクト時に受け取ったauthCodeをもとにアクセストークンを取得する。
取得したアクセストークンを保存する処理は$saveTokenFunctionという名前のコールバック関数として定義する。

app\Services\GooglePhotoService.php
    public $saveTokenFunction = null;

    public function getAccessToken($authCode)
    {
        $client = $this->createGoogleClient();
        $accessToken = $client->fetchAccessTokenWithAuthCode($authCode);
        if ($this->saveTokenFunction != null) {
            call_user_func($this->saveTokenFunction, $accessToken);
        }
        return $accessToken;
    }
app\Http\Controllers\PhotoController.php
    public function saveToken($accessToken)
    {
        // 保存の処理は省略。データベースなどに保存
    }

    public function redirect(Request $request)
    {
        $authCode = $request->input('code');
        $service = new GooglePhotoService();
        $service->saveTokenFunction = [$this, 'saveToken'];
        $service->getAccessToken($authCode);
        return view('redirect', [
        ]);
    }
resources\views\redirect.blade.php
// 省略
routes\web.php
Route::get('/photo/redirect, 'PhotoController@redirect);

PhotosLibraryClientを生成

取得したアクセストークンで、PhotosLibraryClientを生成
アクセストークンの期限が切れたときのリフレッシュの処理も一緒に実装

app\Services\GooglePhotoService.php
    public function getPhotosLibraryClient($accessToken)
    {
        $client = $this->createGoogleClient();
        $client->setAccessToken(json_decode($accessToken, true));
        if ($client->isAccessTokenExpired())
        {
            $client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
            if ($this->saveTokenFunction != null) {
                call_user_func($this->saveTokenFunction, $$client->getAccessToken());
            }
        }

        $authCredentials = new UserRefreshCredentials(\Google_Service_PhotosLibrary::PHOTOSLIBRARY_READONLY, [
            "client_id" => $client->getClientId(),
            "client_secret" => $client->getClientSecret(),
            "refresh_token" => $client->getRefreshToken()
        ]);
        return new PhotosLibraryClient(['credentials' => $authCredentials]);
    }

PhotosLibraryClientを使用してGoogle Photoからアルバム情報を取得してみる

app\Http\Controllers\PhotoController.php
    public function listAlbums()
    {
        $accessToken = [] // 保存してあるアクセストークンを取得する処理
        // 例) $accessToken = UserToken::where('user_id', auth()->user()->id)->value('token');
        $service = new GooglePhotoService();
        $service->saveTokenFunction = [$this, 'saveToken'];
        $client = $service->getPhotosLibraryClient($accessToken);
        $pagedResponse = $client->listAlbums();
        $data = [];
        foreach ($pagedResponse->iterateAllElements() as $album) {
            $data[] = $album->getTitle();
        }
        return response()->json([
            "title" => $data
        ]);
    }
routes\web.php
Route::get('/photo/albums', 'PhotoController@listAlbums');

以上です

authCodeからAccessTokenを取得する処理を、なんとかAPIだけで完結させたかったのですが
できませんでした。今回は、このような形でウェブページを一度経由してその後APIで利用するという形になりました。

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

#MacでLaravelの環境構築をした件

MacでLaravelの環境構築をした件

MacでLaravelの環境構築をした際の内容をまとめます.

目次

動作環境

OS : macOS Mojave 10.14.6
HOMEBREW_VERSION: 2.2.17
Composer version 1.10.6
Laravel Installer 2.3.0
PHP : 7.2.31

構築手順

homebrewのインストール

Homebrewはmacのパッケージマネージャです。
パスワードを求められたらMacのログインパスワードを適宜入力してください

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

インストールが完了したらアップデートをして完了です.

$ brew upgrade

composerのインストール

brew install composer

インストールが完了したらPATHを追加し、インストールできているか確認します.

PATHの追加とバージョン確認
$ echo export PATH=\"$HOME/.composer/vendor/bin:\$PATH\" >> ~/.bash_profile
$ source .bash_profile
$ composer -V
Composer version 1.10.6 2020-05-06 10:28:10

phpのインストール

PHPのバージョンを確認

$ php -v
PHP 7.1.23 (cli) (built: Feb 22 2019 22:19:32) ( NTS )

注意点

7.1系でLaravel newを行うと以下のエラーが出たため7.2以上をインストールします.

Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - This package requires php ^7.2.5 but your PHP version (7.1.23) does not satisfy that requirement.

7.2系のインストール

brew install php@7.2
PATHの追加とバージョン確認
$ echo 'export PATH="/usr/local/opt/php@7.2/bin:$PATH"' >> ~/.bash_profile
$ echo 'export PATH="/usr/local/opt/php@7.2/sbin:$PATH"' >> ~/.bash_profile
$ source ~/.bash_profile
$ php -v
PHP 7.2.31 (cli) (built: May 14 2020 10:55:21) ( NTS )

laravelのインストール

laravelのインストール
$ composer global require "laravel/installer"

注意点

WEBサイトで以下のコマンドを書いていることがありましたが、このコマンドではエラーになります.詳細はこちらをご参考ください

$ composer global require “laravel/install”

動作確認

最後にアプリを作成するディレクトリに移動し実行

$ laravel new dg_laravel_test

サーバーを起動

$ php artisan serve
Laravel development server started: http://127.0.0.1:8000
[Tue May 19 16:52:04 2020] 127.0.0.1:56892 [200]: /favicon.ico

ブラウザからhttp://127.0.0.1:8000にアクセス
以下の画面がでれば成功

スクリーンショット 2020-05-19 16.56.21.png

おわりに

今回の件で以下のことを学びました。
1. Mac OSでのLaravelの環境構築

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

containすると全件表示されない。

DBを直接みると見れるデータが関連ターブルをcontainするとfindで取得できない時の解決法。

/src/Model/Table内の「****Table.php」を修正することで解決しました。
bakeを使ってモデルを生成したときに、joinTypeがINNERになっているのでLEFTに書き換え。

    $this->belongsTo('Authorities', [
        'foreignKey' => 'authority_id',
        'joinType' => 'INNSER',
    ]);

↓↓

    $this->belongsTo('Authorities', [
        'foreignKey' => 'authority_id',
        'joinType' => 'LEFT',
    ]);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP Slim3フレームワークのサンプルアプリを作ろう(2-1. First Application Walkthrough Getting Set Upまで)

はじめに

PHP Slim3フレームワークの勉強のため、
Slim公式のユーザーガイドにあるFirst Application Walkthroughのサンプルアプリを作成します。
まずは、ビルトインWebサーバーで表示させるところまで。

今回は、skeleton projectを使用しないバージョンです。

前提

下記記事で構築した環境を前提とします。

使用ツール

  • Tera Term

手順

1. 専用のユーザーを作成
2. プロジェクトディレクトリを作成
3. Slim Frameworkをインストール
4. アプリケーションを作成
5. アプリケーションを動作させてみる

やってみよう

1. 専用のユーザーを作成

前回の記事と同じ環境で行う方はこの工程はスルーでOKです。

Composerをrootユーザーで操作することは推奨されていないので、
以下のコマンドでslimuserというユーザーを作成します。

useradd slimuser

作成したslimuserに以下のコマンドで切り替えます。

su slimuser

2. プロジェクトディレクトリを作成

slimuserユーザーが作成されると
/home/ディレクトリ内にslimuserディレクトリが作成されます。
その中に以下のプロジェクトディレクトリを作成します。

/home/slimuser/projects/slim/FirstApplication

FirstApplicationの下に以下のようにディレクトリを作成します。
必ずしもこのディレクトリ構造ではなくてもいいようですが、
今回はユーザーガイドに倣います。

.
├── FirstApplication
│   └── src
│       └── public

3. Slim Frameworkをインストール

上記で作成したsrcディレクトリまで移動します。

cd /home/slimuser/projects/slim/FirstApplication/src

以下のコマンドを実行します。

composer require slim/slim:3.*

srcディレクトリ内には以下のようなファイル群が生成されます。

[slimuser@localhost src]$ ls -la
total 16
drwxrwxr-x 4 slimuser slimuser   76 Jun  1 00:30 .
drwxrwxr-x 3 slimuser slimuser   17 Jun  1 00:28 ..
-rw-rw-r-- 1 slimuser slimuser   54 Jun  1 00:30 composer.json
-rw-rw-r-- 1 slimuser slimuser 9700 Jun  1 00:30 composer.lock
drwxrwxr-x 2 slimuser slimuser   23 Jun  1 01:10 public
drwxrwxr-x 7 slimuser slimuser   92 Jun  1 00:30 vendor

4. アプリケーションを作成

srcディレクトリの下のpublicディレクトリに、index.phpファイルを作成します。

vi public/index.php

以下のように記述してください。
First Application Walkthroughでは、
5行目をrequire '../vendor/autoload.php';と記述していましたが、
同じようにするとなぜかエラーが出てしまったので、
require '/home/slimuser/projects/slim/FirstApplication/src/vendor/autoload.php';
のようにフルパスで記述しました。

index.php
<?php
use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;

require '/home/slimuser/projects/slim/FirstApplication/src/vendor/autoload.php';

$app = new \Slim\App;
$app->get('/hello/{name}', function (Request $request, Response $response, array $args) {
    $name = $args['name'];
    $response->getBody()->write("Hello, $name");

    return $response;
});
$app->run();

5. アプリケーションを動作させてみる

今回は、PHPのビルトインWebサーバーを使用して表示します。
以下のコマンドを実行します。
192.168.33.60部分は各自の環境に置き換えてください。

php -S 192.168.33.60:8080 -t public public/index.php

下記が表示されたらブラウザでhttp://192.168.33.60:8080/hello/worldを開いてみてください。

[slimuser@localhost src]$ php -S 192.168.33.60:8080 -t public public/index.php
PHP 7.1.33 Development Server started at Mon Jun  1 02:01:37 2020
Listening on http://192.168.33.60:8080
Document root is /home/slimuser/projects/slim/FirstApplication/src/public
Press Ctrl-C to quit.

"Hello, world"と表示されました。

firstApplication1.png

"world"を"cat"に書き換えて表示すると、表示が変わります。
http://192.168.33.60:8080/hello/cat

firstApplication2.png

参考サイト

First Application Walkthrough

関連ページ

Windows10にVagrantをを入れてCentOS7をインストールしよう

1. VagrantインストールからVagrantfileを設置まで
2. 仮想マシンの操作
3. WinSCP、Tera Termに秘密鍵でログイン
4. WinSCP、Tera Termにrootユーザーでパスワードログイン
5. zip/unzipをインストール
6. Vagrantにて仮想環境を配布

ローカルでLAMP環境を構築しよう

0. 事前準備
1. Apacheをインストール
2. MySQLをインストール
3. PHPをインストール
4. ファイアウォールとか停止する

Composerをインストール

CentOS7にComposerをインストールしよう

PHP Slim3フレームワークのサンプルアプリを作ろう

1. skeleton project
2-1. First Application Walkthrough Getting Set Upまで

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

PHP Slim3フレームワークのサンプルアプリを作ろう(2-1. First Application Walkthrough)

はじめに

PHP Slim3フレームワークの勉強のため、
Slim公式のユーザーガイドにあるFirst Application Walkthroughのサンプルアプリを作成します。
まずは、ビルトインWebサーバーで表示させるところまで。

今回は、skeleton projectを使用しないバージョンです。

前提

下記記事で構築した環境を前提とします。

使用ツール

  • Tera Term

手順

1. 専用のユーザーを作成
2. プロジェクトディレクトリを作成
3. Slim Frameworkをインストール
4. アプリケーションを作成
5. アプリケーションを動作させてみる

やってみよう

1. 専用のユーザーを作成

前回の記事と同じ環境で行う方はこの工程はスルーでOKです。

Composerをrootユーザーで操作することは推奨されていないので、
以下のコマンドでslimuserというユーザーを作成します。

useradd slimuser

作成したslimuserに以下のコマンドで切り替えます。

su slimuser

2. プロジェクトディレクトリを作成

slimuserユーザーが作成されると
/home/ディレクトリ内にslimuserディレクトリが作成されます。
その中に以下のプロジェクトディレクトリを作成します。

/home/slimuser/projects/slim/FirstApplication

FirstApplicationの下に以下のようにディレクトリを作成します。
必ずしもこのディレクトリ構造ではなくてもいいようですが、
今回はユーザーガイドに倣います。

.
├── FirstApplication
│   └── src
│       └── public

3. Slim Frameworkをインストール

上記で作成したsrcディレクトリまで移動します。

cd /home/slimuser/projects/slim/FirstApplication/src

以下のコマンドを実行します。

composer require slim/slim:3.*

srcディレクトリ内には以下のようなファイル群が生成されます。

[slimuser@localhost src]$ ls -la
total 16
drwxrwxr-x 4 slimuser slimuser   76 Jun  1 00:30 .
drwxrwxr-x 3 slimuser slimuser   17 Jun  1 00:28 ..
-rw-rw-r-- 1 slimuser slimuser   54 Jun  1 00:30 composer.json
-rw-rw-r-- 1 slimuser slimuser 9700 Jun  1 00:30 composer.lock
drwxrwxr-x 2 slimuser slimuser   23 Jun  1 01:10 public
drwxrwxr-x 7 slimuser slimuser   92 Jun  1 00:30 vendor

4. アプリケーションを作成

srcディレクトリの下のpublicディレクトリに、index.phpファイルを作成します。

vi public/index.php

以下のように記述してください。
First Application Walkthroughでは、
5行目をrequire '../vendor/autoload.php';と記述していましたが、
同じようにするとなぜかエラーが出てしまったので、
require '/home/slimuser/projects/slim/FirstApplication/src/vendor/autoload.php';
のようにフルパスで記述しました。

index.php
<?php
use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;

require '/home/slimuser/projects/slim/FirstApplication/src/vendor/autoload.php';

$app = new \Slim\App;
$app->get('/hello/{name}', function (Request $request, Response $response, array $args) {
    $name = $args['name'];
    $response->getBody()->write("Hello, $name");

    return $response;
});
$app->run();

5. アプリケーションを動作させてみる

今回は、PHPのビルトインWebサーバーを使用して表示します。
以下のコマンドを実行します。
192.168.33.60部分は各自の環境に置き換えてください。

php -S 192.168.33.60:8080 -t public public/index.php

下記が表示されたらブラウザでhttp://192.168.33.60:8080/hello/worldを開いてみてください。

[slimuser@localhost src]$ php -S 192.168.33.60:8080 -t public public/index.php
PHP 7.1.33 Development Server started at Mon Jun  1 02:01:37 2020
Listening on http://192.168.33.60:8080
Document root is /home/slimuser/projects/slim/FirstApplication/src/public
Press Ctrl-C to quit.

"Hello, world"と表示されました。

firstApplication1.png

"world"を"cat"に書き換えて表示すると、表示が変わります。
http://192.168.33.60:8080/hello/cat

firstApplication2.png

参考サイト

First Application Walkthrough

関連ページ

Windows10にVagrantをを入れてCentOS7をインストールしよう

1. VagrantインストールからVagrantfileを設置まで
2. 仮想マシンの操作
3. WinSCP、Tera Termに秘密鍵でログイン
4. WinSCP、Tera Termにrootユーザーでパスワードログイン
5. zip/unzipをインストール
6. Vagrantにて仮想環境を配布

ローカルでLAMP環境を構築しよう

0. 事前準備
1. Apacheをインストール
2. MySQLをインストール
3. PHPをインストール
4. ファイアウォールとか停止する

Composerをインストール

CentOS7にComposerをインストールしよう

PHP Slim3フレームワークのサンプルアプリを作ろう

1. skeleton project
2-1. First Application Walkthrough

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

PHP Slim3フレームワークのサンプルアプリを作ろう(2-1. アプリケーションの作成・表示)

はじめに

PHP Slim3フレームワークの勉強のため、
Slim公式のユーザーガイドにあるFirst Application Walkthroughのサンプルアプリを作成します。
まずは、ビルトインWebサーバーで表示させるところまで。

今回は、skeleton projectを使用しないバージョンです。

前提

下記記事で構築した環境を前提とします。

使用ツール

  • Tera Term

手順

1. 専用のユーザーを作成
2. プロジェクトディレクトリを作成
3. Slim Frameworkをインストール
4. アプリケーションを作成
5. アプリケーションを動作させてみる

やってみよう

1. 専用のユーザーを作成

前回の記事と同じ環境で行う方はこの工程はスルーでOKです。

Composerをrootユーザーで操作することは推奨されていないので、
以下のコマンドでslimuserというユーザーを作成します。

useradd slimuser

作成したslimuserに以下のコマンドで切り替えます。

su slimuser

2. プロジェクトディレクトリを作成

slimuserユーザーが作成されると
/home/ディレクトリ内にslimuserディレクトリが作成されます。
その中に以下のプロジェクトディレクトリを作成します。

/home/slimuser/projects/slim/FirstApplication

FirstApplicationの下に以下のようにディレクトリを作成します。
必ずしもこのディレクトリ構造ではなくてもいいようですが、
今回はユーザーガイドに倣います。

.
├── FirstApplication
│   └── src
│       └── public

3. Slim Frameworkをインストール

上記で作成したsrcディレクトリまで移動します。

cd /home/slimuser/projects/slim/FirstApplication/src

以下のコマンドを実行します。

composer require slim/slim:3.*

srcディレクトリ内には以下のようなファイル群が生成されます。

[slimuser@localhost src]$ ls -la
total 16
drwxrwxr-x 4 slimuser slimuser   76 Jun  1 00:30 .
drwxrwxr-x 3 slimuser slimuser   17 Jun  1 00:28 ..
-rw-rw-r-- 1 slimuser slimuser   54 Jun  1 00:30 composer.json
-rw-rw-r-- 1 slimuser slimuser 9700 Jun  1 00:30 composer.lock
drwxrwxr-x 2 slimuser slimuser   23 Jun  1 01:10 public
drwxrwxr-x 7 slimuser slimuser   92 Jun  1 00:30 vendor

4. アプリケーションを作成

srcディレクトリの下のpublicディレクトリに、index.phpファイルを作成します。

vi public/index.php

以下のように記述してください。
First Application Walkthroughでは、
5行目をrequire '../vendor/autoload.php';と記述していましたが、
同じようにするとなぜかエラーが出てしまったので、
require '/home/slimuser/projects/slim/FirstApplication/src/vendor/autoload.php';
のようにフルパスで記述しました。

index.php
<?php
use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;

require '/home/slimuser/projects/slim/FirstApplication/src/vendor/autoload.php';

$app = new \Slim\App;
$app->get('/hello/{name}', function (Request $request, Response $response, array $args) {
    $name = $args['name'];
    $response->getBody()->write("Hello, $name");

    return $response;
});
$app->run();

5. アプリケーションを動作させてみる

今回は、PHPのビルトインWebサーバーを使用して表示します。
以下のコマンドを実行します。
192.168.33.60部分は各自の環境に置き換えてください。

php -S 192.168.33.60:8080 -t public public/index.php

下記が表示されたらブラウザでhttp://192.168.33.60:8080/hello/worldを開いてみてください。

[slimuser@localhost src]$ php -S 192.168.33.60:8080 -t public public/index.php
PHP 7.1.33 Development Server started at Mon Jun  1 02:01:37 2020
Listening on http://192.168.33.60:8080
Document root is /home/slimuser/projects/slim/FirstApplication/src/public
Press Ctrl-C to quit.

"Hello, world"と表示されました。

firstApplication1.png

"world"を"cat"に書き換えて表示すると、表示が変わります。
http://192.168.33.60:8080/hello/cat

firstApplication2.png

参考サイト

First Application Walkthrough

関連ページ

Windows10にVagrantをを入れてCentOS7をインストールしよう

1. VagrantインストールからVagrantfileを設置まで
2. 仮想マシンの操作
3. WinSCP、Tera Termに秘密鍵でログイン
4. WinSCP、Tera Termにrootユーザーでパスワードログイン
5. zip/unzipをインストール
6. Vagrantにて仮想環境を配布

ローカルでLAMP環境を構築しよう

0. 事前準備
1. Apacheをインストール
2. MySQLをインストール
3. PHPをインストール
4. ファイアウォールとか停止する

Composerをインストール

CentOS7にComposerをインストールしよう

PHP Slim3フレームワークのサンプルアプリを作ろう

1. skeleton project
2-1. アプリケーションの作成・表示

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

PHP Slim3フレームワークのサンプルアプリを作ろう(2-1. Getting Set Up)

はじめに

PHP Slim3フレームワークの勉強のため、
Slim公式のユーザーガイドにあるFirst Application Walkthroughのサンプルアプリを作成します。
まずは、ビルトインWebサーバーで表示させるところまで。

今回は、skeleton projectを使用しないバージョンです。

前提

下記記事で構築した環境を前提とします。

使用ツール

  • Tera Term

手順

1. 専用のユーザーを作成
2. プロジェクトディレクトリを作成
3. Slim Frameworkをインストール
4. アプリケーションを作成
5. アプリケーションを動作させてみる

やってみよう

1. 専用のユーザーを作成

前回の記事と同じ環境で行う方はこの工程はスルーでOKです。

Composerをrootユーザーで操作することは推奨されていないので、
以下のコマンドでslimuserというユーザーを作成します。

useradd slimuser

作成したslimuserに以下のコマンドで切り替えます。

su slimuser

2. プロジェクトディレクトリを作成

slimuserユーザーが作成されると
/home/ディレクトリ内にslimuserディレクトリが作成されます。
その中に以下のプロジェクトディレクトリを作成します。

/home/slimuser/projects/slim/FirstApplication

FirstApplicationの下に以下のようにディレクトリを作成します。
必ずしもこのディレクトリ構造ではなくてもいいようですが、
今回はユーザーガイドに倣います。

.
├── FirstApplication
│   └── src
│       └── public

3. Slim Frameworkをインストール

上記で作成したsrcディレクトリまで移動します。

cd /home/slimuser/projects/slim/FirstApplication/src

以下のコマンドを実行します。

composer require slim/slim:3.*

srcディレクトリ内には以下のようなファイル群が生成されます。

[slimuser@localhost src]$ ls -la
total 16
drwxrwxr-x 4 slimuser slimuser   76 Jun  1 00:30 .
drwxrwxr-x 3 slimuser slimuser   17 Jun  1 00:28 ..
-rw-rw-r-- 1 slimuser slimuser   54 Jun  1 00:30 composer.json
-rw-rw-r-- 1 slimuser slimuser 9700 Jun  1 00:30 composer.lock
drwxrwxr-x 2 slimuser slimuser   23 Jun  1 01:10 public
drwxrwxr-x 7 slimuser slimuser   92 Jun  1 00:30 vendor

4. アプリケーションを作成

srcディレクトリの下のpublicディレクトリに、index.phpファイルを作成します。

vi public/index.php

以下のように記述してください。
First Application Walkthroughでは、
5行目をrequire '../vendor/autoload.php';と記述していましたが、
同じようにするとなぜかエラーが出てしまったので、
require '/home/slimuser/projects/slim/FirstApplication/src/vendor/autoload.php';
のようにフルパスで記述しました。

index.php
<?php
use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;

require '/home/slimuser/projects/slim/FirstApplication/src/vendor/autoload.php';

$app = new \Slim\App;
$app->get('/hello/{name}', function (Request $request, Response $response, array $args) {
    $name = $args['name'];
    $response->getBody()->write("Hello, $name");

    return $response;
});
$app->run();

5. アプリケーションを動作させてみる

今回は、PHPのビルトインWebサーバーを使用して表示します。
以下のコマンドを実行します。
192.168.33.60部分は各自の環境に置き換えてください。

php -S 192.168.33.60:8080 -t public public/index.php

下記が表示されたらブラウザでhttp://192.168.33.60:8080/hello/worldを開いてみてください。

[slimuser@localhost src]$ php -S 192.168.33.60:8080 -t public public/index.php
PHP 7.1.33 Development Server started at Mon Jun  1 02:01:37 2020
Listening on http://192.168.33.60:8080
Document root is /home/slimuser/projects/slim/FirstApplication/src/public
Press Ctrl-C to quit.

"Hello, world"と表示されました。

firstApplication1.png

"world"を"cat"に書き換えて表示すると、表示が変わります。
http://192.168.33.60:8080/hello/cat

firstApplication2.png

参考サイト

First Application Walkthrough

関連ページ

Windows10にVagrantをを入れてCentOS7をインストールしよう

1. VagrantインストールからVagrantfileを設置まで
2. 仮想マシンの操作
3. WinSCP、Tera Termに秘密鍵でログイン
4. WinSCP、Tera Termにrootユーザーでパスワードログイン
5. zip/unzipをインストール
6. Vagrantにて仮想環境を配布

ローカルでLAMP環境を構築しよう

0. 事前準備
1. Apacheをインストール
2. MySQLをインストール
3. PHPをインストール
4. ファイアウォールとか停止する

Composerをインストール

CentOS7にComposerをインストールしよう

PHP Slim3フレームワークのサンプルアプリを作ろう

1. skeleton project
2-1. Getting Set Up

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

Laravelでコマンドライン処理を行う

前提条件

eclipseでLaravel開発環境を構築する。デバッグでブレークポイントをつけて止める。(WindowsもVagrantもdockerも)
本記事は上記が完了している前提で書かれています
プロジェクトの作成もapacheの設定も上記で行っています

Commandクラス作成

コマンドラインで
cd sample
php artisan make:command SampleCommand
xdebugの設定をしているとeclipseが実行していいですかというプロンプトを出すのでOKを押します
eclipseプロジェクトを右クリック→リフレッシュ
/sample/app/Console/Commands/SampleCommand.phpが現れます

Commandクラス修正

さきほど作成したSampleCommand.phpを下記に修正します

SampleCommand.php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class SampleCommand extends Command
{
    protected $signature = 'sample:name1 {arg1} {arg2=val2} {arg3?} {--option1} {--option2=} {--option3=opVal3} {--option4=*}';

    protected $description = 'このプログラムの説明を書く';

    public function __construct()
    {
        parent::__construct();
    }

    public function handle()
    {
        $arg1 = $this->argument('arg1');
        $arg2 = $this->argument('arg2');
        $arg3 = $this->argument('arg3');
        $option1 = $this->option('option1');
        $option2 = $this->option('option2');
        $option3 = $this->option('option3');
        $option4 = $this->option('option4');

        $this->line('下記入力を受け付けました');
        $this->line('arg1:' . $arg1);
        $this->line('arg2:' . $arg2);
        $this->line('arg3:' . $arg3);
        $this->line('option1:' . $option1);
        $this->line('option2:' . $option2);
        $this->line('option3:' . $option3);
        $this->line('option4:' . var_export($option4, true));

        $in = $this->ask('何か入力してください');
        $this->line('下記入力を受け付けました');
        $this->line($in);

        $in = $this->secret('何か入力してください。この入力中はユーザーがタイプした値を表示しません');
        $this->line('下記入力を受け付けました');
        $this->line($in);

        if ($this->confirm('yかyesを入力するとtrueとして扱います')) {
            $this->line('true');
        } else {
            $this->line('false');
        }

        $in = $this->choice(
                  '選択入力。カンマ区切りで複数入力できます',
                  ['php', 'Laravel', 'apache', 'eclipse'], //選択肢
                  $defaultIndex = 1, // 何も選ばれなかった場合に返ってくる選択肢の要素
                  $maxAttempts = 2, // 最大選択可能個数
                  $allowMultipleSelections = true // 複数選択可ならtrueにする
            );
        $this->line('下記入力を受け付けました');
        $this->line(var_export($in, true));

        $this->line('テーブル出力');
        $head = ['col1', 'col2', 'col3', 'col4'];
        $body = [
            ['1-col1', '1-col2', '1-col3', '1-col4'],
            ['2-col1', '2-col2', '2-col3', '2-col4'],
            ['3-col1', '3-col2', '3-col3', '3-col4'],
        ];
        $this->table($head, $body);


        $this->line('プログレスバー出力');
        $bar = $this->output->createProgressBar(10); // プログレスバーを10の区切りで出力
        $bar->start();
        for ($i = 0; $i < 10; $i++) {
            sleep(1);
            $bar->advance();
        }
        $bar->finish();

        $this->line('');
        $this->error('エラー出力');

    }
}

$signature変数はこのコマンドの引数、オプションを定義します
ユーザーから入力してもらう引数とオプションはすべて波括弧で囲んで定義します
{arg1} 第1引数必須。プログラム内で$this->argument('arg1')で値を取得できる
{arg2=val2} 第2引数任意。入力されなかった場合、$this->argument('arg2')でval2が返る
{arg3?} 第3引数任意。デフォルト値無し
{--option1} オプション。option1オプションが指定されると$this->option('option1')でtrueが返る
{--option2=} オプション。コマンド実行時何か値を与えるオプション
{--option3=opVal3} オプション。入力されなかった場合、$this->option('option3')でopVal3が返る
{--option4=*} オプション。コマンド実行時何か値を与えるオプション。複数の値を渡すことができる。$this->option('option4')の結果は配列となる

$description変数はこのコマンドの説明です

handleメソッドは実際の処理です
コマンドライン処理が実行されるとhandleメソッドが実行されます
$this->argumentは引数を取得できます
$this->optionはオプションを取得できます
$this->lineは文字列を出力できます
$this->askはユーザーからの入力を待ちます
$this->secretはユーザーからの入力を待ちます。ただし、ユーザーのタイピングした値が表示されません。パスワード入力などに使います
$this->confirmはユーザーの入力によりbool値を返します
$this->choiceはユーザーに選択肢から値を選択させます
$this->tableはテーブル形式で表示されます
$this->output->createProgressBarの戻り値を使うことによってプログレスバーを表示できます
$this->errorでエラー時の文字列出力ができます

Commandクラス登録

作成したCommandクラスを呼び出せるようにLaravelに登録しましょう
/sample/app/Console/Kernel.php修正

Kernel.php
‥‥
    protected $commands = [
        Commands\SampleCommand::class
    ];

‥‥

Kernel.phpに定義されている$commands配列に先ほど作成したSampleCommandクラス名を追記します
これでコマンドラインで実行できるようになりました

動作確認

コマンドラインで
cd sample
php artisan sample:name1 aaa bbb ccc --option1 --option2=eee --option3=fff --option4=ggg --option4=hhh
先ほど作成したSampleCommandクラスの$signature変数に定義した通りにコマンドラインで実行します
xdebugの設定をしているとeclipseが実行していいですかというプロンプトを出すのでOKを押します

実行結果

下記入力を受け付けました
arg1:aaa
arg2:bbb
arg3:ccc
option1:1
option2:eee
option3:fff
option4:array (
  0 => 'ggg',
  1 => 'hhh',
)

 何か入力してください:
 > aaa

下記入力を受け付けました
aaa

 何か入力してください。この入力中はユーザーがタイプした値を表示しません:
 >

下記入力を受け付けました
bbb

 yかyesを入力するとtrueとして扱います (yes/no) [no]:
 > y

true

 選択入力。カンマ区切りで複数入力できます [Laravel]:
  [0] php
  [1] Laravel
  [2] apache
  [3] eclipse
 > 2,3

下記入力を受け付けました
array (
  0 => 'apache',
  1 => 'eclipse',
)
テーブル出力
+--------+--------+--------+--------+
| col1   | col2   | col3   | col4   |
+--------+--------+--------+--------+
| 1-col1 | 1-col2 | 1-col3 | 1-col4 |
| 2-col1 | 2-col2 | 2-col3 | 2-col4 |
| 3-col1 | 3-col2 | 3-col3 | 3-col4 |
+--------+--------+--------+--------+
プログレスバー出力
 10/10 [============================] 100%
エラー出力

実行できました

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

PHPのフレームワーク作ってみました。

■PHPフレームワーク(Webシステム用)の概要

久しぶりに?PHPを触る機会があったので、フレームワークを作ってみました。
コンセプトは、

  • スモールスタートできる。
  • あんまり準備が必要ない。
  • 直感的にわかる。

です。
なぜフレームワークを作ったかと言うとLaravelとかCakePHPとか使いづらい、と言うか作りづらいと思うからです。
車輪の再発明だと言われるかもしれませんが、世の中にたくさんのフレームワークが存在しているわけですし、今更1つくらい増えても問題はないでしょと思います。

ちなみに名前は、安易ですが「WSI: Web System Infrastructure(Webシステム基盤)」としています。

★Controllerを作るのは面倒

LaravelもCakePHPも、その他のフレームワークもほとんどの場合、Controllerを作成します。
Controllerで処理をした後Viewに制御を渡して、画面を表示します。

プロトタイプなど作成すると、ほとんどの場合、HTMLと紙芝居用のJavaScript、見た目のデザイン用のCSSしか作りません。
でも、そのHTMLとかJavaScriptとか、CSSとかをいざアプリケーションとして開発するために利用すると、結構、多大な変更が必要になります。

変更が必要な原因は、HTML/JavaScript/CSSがアプリケーション様に部品化されていないのがほとんどですが、それは動作イメージを捉えるために、システマチックな”部品化”などと言う考えを排除しているためです。
それでもデザインツールなどで作成すれば、部品化(と言うか、パターン化)されるわけですけど。

それよりも、例えばURLの書き換え(プロトタイプでは相対URLなどで書いていることが多い)をするのが意外に面倒です。
また、最近はRESTfullと言うのが流行りらしく、URLの構造がAPI的な構造だったりします。
でも、プロトタイプの時は、HTML/JavaScript/CSSを「.html」「.js」「*.css」としています。
それを「/xxx/yyy/zzzz」みたいなURLに修正する必要があります。

あと、LaravelとかはControllerをartisanコマンドとかで作成しなければならないです。
「手動で作れるよ」とか言われそうですが、結局Controllerはルールがあるので、開発者が多いほどartisanコマンドで、生成するべきです。
Controllerくらいは大したことないですけど…。
そして、実際のHTMLファイルなどはView専用のディレクトリに配置して、Controllerから制御を渡すコードを書く必要があります。

例えば、プロトタイプで作成した「index.html」があったとします。
それを、そのままブラウザで見られるようにすると良いと思います。
※これだけでは処理(Controllerなどに実装する処理)が書けないので、それは後程説明します。

[root]
+ index.html

このような場合に、ブラウザで「http://xxxx.yyy.zzz/index.html」を表示したら、上記のindex.htmlが表示されるようにしています。
だから、基本的にルーティングもいらない

★Viewに対する処理は簡単に追加したい

上記で書いたContollerの話にも関連しますが、Viewに対する処理は簡単に追加できるようにしたいです。
具体的には、上記のファイル構成の場合に以下のようにします。

[root]
+ index.html
+ index.html.php

このように、ファイル名+.phpとすることで、そのファイルを表示する際に実行する処理を追加できます。
index.html.phpの中身(必要最低限)は下記のとおりです。

use WSI\Request;
use WSI\Status;

return function(Request $request) {
    return Status::ok();
};

例えば、以下のようにするとindex.html(View)に変数を渡せます。

// index.html.phpファイルのコード
use WSI\Request;
use WSI\Status;

return function(Request $request) {
    return Status::ok()->set_param('message', 'Hello world !!');
};
<!-- index.htmlファイルのコード -->
<!DOCTYPE html>
<html>
<body>
<?=$status->get_param('message') ?>
</body>
</html>

このように簡単に処理コードを追加できます。
ボタンをクリックした場合の処理はどうやって追加するのか?
それは後で説明します。

見てわかる通り、基本的にHTMLファイル(それ以外のファイルもですが…)は、phpソースとしてincludeしています。
テンプレートエンジンは使いません(php自体がテンプレートエンジンのようなものなので…)。
これにより、新しいテンプレートの構文を覚える必要はないですし、Viewのコーディングの自由度が上がります。

★データベースはSQLビルダーとか使いたくない

LaravelもCakePHPも、その他のPHPフレームワークも、SQLビルダー(もしくはそれに類するもの)を使えるようになっています。
確かにSQLビルダーを使うことでSQL(で実現したいこと)を抽象化できますし、他のDBに移行するのも簡単になります。
しかし、SQLビルダーのルールは覚えなければならないですし、SQLビルダーによってどのようなSQLを実行しているのか分かりづらくなります。
ほとんどの場合、DB開発用のツールを使ってSQLを考えていると思います。
そうなった場合、いちいちSQL文をSQLビルダー構文に変換しなければなりません。
また、その逆もしかりで、何か障害が発生しSQLの解析をするにしても、ログから取得したSQLかSQLビルダーから変換したSQLを元に調査していると思います。
調査した結果でSQLを変更した場合、またSQLビルダー構文に変換しなければなりません。
データベースの移行なんてシチュエーションも、ほとんどありません(その場合は、絶対にSQLを再実装するはずです)。

面倒ですし、無駄が多いですよね?

だったら、SQLのまま使った方が良くないですか?
SQLビルダーはSQLに比べて学習コストが低かったりする(売り文句)様ですが、結局どんなSQL(DBに対する命令)を実行しているのか
イメージしながらSQLビルダーでコーディングしますよね?

本当に面倒です!

DBのアクセスはSQLで実装しましょう。
このフレームワークでは、SQL(と言うかDB)に対してよく使う機能だけをメソッドとして提供します。
とあるテーブルに問い合わせるSQLは、以下のようなコードになります。

-- usersテーブルをユーザーIDで問い合わせるSELECT文です。
-- rootディレクトリ配下に、users\select\all.sqlとして配置されているものとします。
select
  *
from
  users
where
  id = :user_id
  and del_flg = 0
// index.htmlにユーザー情報を表示するために実装した、index.html.phpのコード
use WSI\Request;
use WSI\Status;
use WSI\Database;

return function(Request $request) {
    $db = Database::connect();                           //データベースへの接続(接続先の定義は別にありますが、ここでは割愛します)

    $sql = Resource::from('/users/select/all.sql');      //SQL文を取得する。
    $user = $db->row($sql, ['user_id'='xxxxxx']);        //rowメソッドで結果セットの先頭1行目を取得する。
                                                         //バインド変数は変数名で参照されます(「?」は使わない)。同じ変数名なら同じ値がバインドされます。
    return Status::ok()->set_param('user', $user);
};
<!-- index.htmlファイルのコード -->
<?php
$user = $status->get_param('user');        //取得したusersテーブルのレコードをあえて変数に格納
?>
<!DOCTYPE html>
<html>
<body>
<!--レコードはPDO::FETCH_BOTHで取得したものとなっている-->
ID:&nbsp;<?=$user['id'] ?><br>             
名前:&nbsp;<?=$user['name'] ?><br>
   :
   :
</body>
</html>

煩わしいSQLビルダーは使いません。
素直にSQLを使いましょう。
なお、実はSQLファイルもphpファイルとしてincludeしています。
つまり、SQL文を動的に変更することも可能です。
ただし、SQLファイルをキャッシュしていないので、ループの中でResource::fromを実行しない様にしてください。

★機能別に分けて開発したい

PHPは、COMPOSERによるパッケージ化ができるのですが、開発しているシステム内を簡単にパッケージ化するのには向いていません。
一つのシステムでは、機能A/機能B/機能C…のように分かれていることが多いですが、LaravelやCakePHPはそういった機能ごとに
分割(分担)して開発するのは、あまり得意でない様に思います。
(自分が知らないだけなのかもしれませんが…)
PHPでも(Javaで言うところのjarファイルのように)pharファイル(PHp ARchiveでしたっけ?)が作れますが、実際のシステム開発であまり使われていないような気がします。
機能ごとに開発してpharファイル化したものを本番環境に配置することでリリースができたら、管理も楽になると思います。

もちろん、1システムが1セットで開発することのメリットはあります。
最近では開発に構成管理ツール(Subversionやgitなど)を導入することは多いですので、チェックアウト(gitだとクローン)すればフルセットで開発に必要な資材が手元のPCに簡単に用意できるわけです。
それ以外にも、他の機能のコードも含んでいるので調査したり解析することもできます。

しかし、やはり弊害の方が多い気がします。

  • 見えるソースが多いため、思考にノイズが入りやすい。
  • IDEなどによっては、ソース量が多いため多くのメモリが必要になる。
  • 無意識に関係ない機能のソースを修正してしまって、気が付かない場合がある(コミットする際に気が付くとは思いますが…)。
  • 依存関係が見えづらい。

など。
機能別にパッケージ化されてソースが隠蔽されることで、設計(特にインターフェースになる部分)が重要になるわけですが、本質的には設計が重要なのは当たり前です。
具体的にどうなるかと言うと、

  • 公開ディレクトリ ←下記の共通機能、機能A、機能B、機能Cをpharファイルにして配置
  • 共通機能 ←パッケージ
  • 機能A  ←パッケージ(共通機能に依存)
  • 機能B  ←パッケージ(共通機能と機能Aに依存)
  • 機能C  ←パッケージ(共通機能と機能Aに依存)

と言った具合に、機能ごとにパッケージを分けられるようにすれば、部分的な機能別のリリースも可能になります。

なお、当フレームワークでは、最初は1セットとして開発してたものを、機能別に分割(ディレクトリを分けるだけですけど)し、さらに機能別に分割したものをpharファイル化すれば、そのpharファイルを参照して動作する様になっています。
例えば、以下のようなディレクトリ構成でシステムに資材を配置していたとします。

 [root]
    + index.html
    + [view]
        + login.html
        + topmenu.html
        + userlist.html
        + page1.html
        + page2.html
    + [js]
        + jquery.js
        + common.js
        + login.js
        + topmenu.js
        + userlist.js
        + page1.js
        + page2.js
    + [css]
        + bootstrap.js
        + common.css
        + method_a_common.css

これを以下のような構成に分割します。

↓root(公開ディレクトリ)
[root]
    + index.html
↓common(共通機能)
[common]
    + index.html
    [view]
        + login.html
        + topmenu.html
        + userlist.html
    + [js]
        + jquery.js
        + common.js
    + [css]
        + bootstrap.js
        + common.css
↓機能A(ディレクトリ)
[method_a]
    + [view]
        + page1.html
        + page2.html
    + [js]
        + page1.js
        + page2.js
    + [css]
        + common.css      ←元は、method_a_common.css

このように分割すると、それぞれの資源は以下のようにアクセスできます。

  • 公開ディレクトリにある資材… /index.hml など
  • 共通機能の資材… /common/view/login.html など
  • 機能Aの資材… /method_a/view/page1.html など

この状態で、例えば「機能A」をパッケージ化して「method_a.phar」とした場合でも、やはり「/method_a/view/page1.html」と言うように、資材にアクセスするURLは変えません。
もちろん、フレームワークがその様になるように処理しているわけです。

資材の配置について、当フレームワークでは、以下のようなURLでアクセスする様にできます。

  • 設定前)/method_a/view/page1.html
  • 設定後)/method_a/page1.html

これは、「.htmlファイルは、viewディレクトリ配下に配置している」と設定しているためです。
同様に.jsはjsディレクトリ、.cssはcssディレクトリに配置すると設定できます。
このように、アクセスする資材の拡張子によって、ディレクトリの構造を決めることが可能です。

もう一つ、公開ディレクトリの特殊な処理についてです。
例えば、rootディレクトリ(公開ディレクトリ)内に、view/common/topmenu.htmlとなるように資材を配置したとします。
この場合、本来のcommonディレクトリ(共通機能)に格納されている資材よりも優先的に参照するようになっています。
つまり「修正してみてどう変わるか」を簡単に検証できます。
topmenu.htmlをコピーしてroot配下に修正したものを配置すると、修正内容を試すことが可能です。

また、method_a全体に波及するような修正を施し、method_a1として配置します。
そのmethod_a配下の資材を参照するURLを書き換えた資材(例えば、aタグのhrefを変更したtopmenu.html)を、root配下に配置します。
そうすると、元のmethod_aディレクトリ及びtopmenu.htmlを変更せずに、修正を試すことが可能になります。
想定通りに動作することを確認した後は、method_aをmethod_a1に挿げ替え、topmenu.htmlをcommonディレクトリ配下に配置すれば、不具合の発生を抑えることが可能になります。

★やっぱりAjaxですよね

リクエストに対してViewの全てをレスポンスする場合、実装は難しくなります。
ここで言う「難しい」の意味は、単にコード量が多いという意味ではありません。

例えば、処理結果で「はい/いいえ」の確認ダイアログを表示して、「はい(または、いいえ)」を選択して処理を継続するような場合に、従来の画面リフレッシュ方式のWeb実装(こういうのをMVCと言えばよいのかな?)では、ちょっとしたテクニックが必要だったりします。
それは、アクションに対する一連の処理(トランザクション)とHTTPによるリクエスト/レスポンスのサイクルは、必ずしも一致しないと言うことに起因します。
また、HTTP通信の役割は画面の再描画であるため、確認ダイアログを表示するために一旦はブラウザにレスポンスしないとなりません。
ブラウザにレスポンスと言う事は、この時点でHTTP通信(リクエスト~レスポンス)の1サイクルは完了しています。
しかし、トランザクションとしては継続中です。
その為、「はい・いいえ」を選択した後は2サイクル目のHTTP通信に対して、1サイクル目のHTTP通信の状態を引き継ぐ必要があります。
このようなシチュエーションは、割とよく発生します。

Ajaxの場合は、HTTP通信の役割がデータのやり取りでしかない(画面の再描画ではない)ため、アクションのトランザクション中の「データ・アクセス」と「画面の描画」が切り離せると言う事です。
(データベースのトランザクションの意味ではないです)
これにより、実装はシンプルになり意味的に理解しやすくなるため、結果として実装が難しくなくなります。

LaravelやCakePHPは、基本的にMVCフレームワークであるため、こういった問題が発生しやすくなります。
(LaravelだとRESTFull APIを実装できるので、設計次第ではAjaxも選択できるわけですが…)

当フレームワークは、一般に言うMVCのフレームワークではなく、Ajax方式のフレームワークになっています。
その為、クライアントで動作する基本JavaScriptとして、以下のような機能を提供しています。

  • Ajax
  • DOM操作

また、上記のほかに、JavaScriptを実装するためのいくつかの機能も提供しています。

  • ネームスペース化
  • クラス化
  • 画面項目のコントロール
  • 画面管理クラス化
  • IFrameをまたいだメッセージ通信

結構長くなったので続きは別の機会に

とりあえず、他にもフレームワークとしての機能は実装していますが、さすがに長くなってきたので一旦区切りとします。
また、まだまだ未完成でもありますので、今後も改善・機能追加は発生します。

以下に、公開しているGitHubのURLを掲載しますので、興味のある方はご参照ください。

github WSIソース&サンプル

今後は、実際のアプリケーションを作成するための実装方法を掲載するようにします。
それでは、また。

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