20201208のPHPに関する記事は29件です。

Laravel Duskエラー 「Facebook\WebDriver\Exception\UnknownErrorException: unknown error: net::ERR_CONNECTION_REFUSED」

発生したタイミング

 テストコード作成後、下記コマンド実行時にタイトル記載のエラーが表示

php artisan dusk

原因

 調べるとどうやら.envに記載したAPP_URLパラメータの値が間違っている模様

原因解決

 .envのAPP_URLパラメータの値をブラウザからアクセスするアプリケーションで使用するURLと一致させる。
 現在デプロイをしておらずローカル環境だった為下記のように変更する。

APP_URL=http://localhost:8000/

修正後コマンド実行

これで解決したと思い再度php artisan duskを実行すると......。
同じエラーが。
.......原因はサーバーを立ち上げていなかったから。
下記コマンドでサーバーを立ち上げ。。。。

php artisan serve

新規タブで

php artisan dusk

これで無事テストが実行されました!!!
以上

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

Laravel Duskの導入

導入背景

 現在Laravelで作成したポートフォリオのテストをデフォルトで搭載されているテストディレクトリを使用して実行していたが、利用が簡単なブラウザの自動操作/テストAPIであるDuckの存在を知ったので使用してみた

導入方法

プロジェクトに移動し、パッケージを取得

cd (プロジェクト名)
composer require --dev laravel/dusk
php artisan dusk:install

上記後、プロジェクトのtestディレクトリ内にBrowserディレクトリが作成される

.envの変更

.envファイル内のAPP_URL環境変数を変更する。
値はブラウザからアクセスするアプリケーションで使用するURLと一致させます。
※ローカル環境の場合

//デフォルト)
APP_URL=http://localhost
↓
//変更後)
APP_URL=http://localhost:8000

参考文献

https://readouble.com/laravel/7.x/ja/dusk.html

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

CakePHPでテキストを切り詰めてサフィックスを付ける

はじめに

テキストを特定の文字数で切り詰めて末尾に...などを付けたいときありますよね。
PHPでの組み込み関数を使えば簡単に実現できるのですが、CakePHPでは更に簡単に使えるものがありました。
最新のCakePHP4のものを紹介します。(2.x、3.xでもありました)

https://book.cakephp.org/4/ja/core-libraries/text.html#Cake\Utility\Text::truncate

使い方

Cookbookに載っている通りになるのですが、
Cake\Utility\Text::truncate(string $text, int $length = 100, array $options)メソッドを利用します。

$text$lengthより長い場合、このメソッドはそれを$lengthの長さに切り詰め、 'ellipsis'が定義されているなら末尾にその文字列を追加します。

ちなみにellipsisは日本語で省略記号という意味です。(truncate切り捨てる

use Cake\Utility\Text;

$text = 'テキストを切り詰めます'

echo Text::truncate(
    $text,
    5,
    [
        'ellipsis' => '...',
    ]
);

出力:

テキストを...

※$textがnullの場合でもサフィックスだけ表示されちゃうみたいなことは起きません。

おわりに

FWを利用する場合、汎用的なメソッドであれば便利な形で用意されてることもあるので、一度FW標準のものがないか確認した上で、なければ組み込み関数を使うといった手順を踏んだ方がよい、と本日先輩に教わりましたので備忘録として書き記しました。

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

Laravel/Socialiteが何をしているか調べてみた ~①リダイレクト編~

目標

Laravelで「Googleでログイン」を実装する際に導入する"Socialite"がどんなことをやっているのか調査してみた。
①リダイレクト編の目標は、Controllerに記述することになる下記コードが最終的なアウトプットは何か、どのように実装されているのかを明確にする。

以下Controllerに記述することになるコード

SocialOAuthController.php
/**
 * Googleアカウントの認証ページへユーザーをリダイレクト
 * ①リダイレクト編で解説するコード
 */
public function redirectToGoogle()
{
    return Socialite::driver('google')->redirect();
}

/**
 * Googleアカウントからユーザー情報を取得
 * ②コールバック編で解説予定
 */
public function handleGoogleCallback()
{
    $user = Socialite::driver('google')->stateless()->user();
}

先に結論(最終的なアウトプット)

認可URLにリダイレクトするRedirectResponseインスタンスを生成します。
リダイレクト先のURLは下記の通りです。

RequestURL = https://accounts.google.com/o/oauth2/auth?
client_id=$client_id
&redirect_url=$redirect_url
&scope=$scope1+$scope2+$scope3
&response_type=code
&state=$state

<環境>

Laravel Framework 8.17.2
laravel/socialite v5.1.2

<解説>

Socialite

SocialiteのファサードはSocialiteServiceProviderで下記のように定義されています。

SocialiteServiceProvider.php
namespace Laravel\Socialite;

class SocialiteServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton(Factory::class, function ($app) {
            return new SocialiteManager($app);
        });
    }
}

Socialite::driver('google')

driverメソッドに文字列でドライバを指定しています。
Socialiteクラスの基底クラスであるManagerクラスにdriverメソッドで引数に渡した文字列に該当するドライバが可変関数として呼び出されます。

Manager
namespace Illuminate\Support;

abstract class Manager
{
    /**
    * Get a driver instance.
    *
    * @param  string|null  $driver
    * @return mixed
    *
    * @throws \InvalidArgumentException
    */
    public function driver($driver = null)
    {
        $driver = $driver ?: $this->getDefaultDriver();

        if (is_null($driver)) {
            throw new InvalidArgumentException(sprintf(
                'Unable to resolve NULL driver for [%s].', static::class
            ));
        }

        // ドライバのインスタンスはキャッシュして再利用される。
        if (! isset($this->drivers[$driver])) {
            $this->drivers[$driver] = $this->createDriver($driver);
        }

        return $this->drivers[$driver];
    }

    /**
    * Create a new driver instance.
    *
    * @param  string  $driver
    * @return mixed
    *
    * @throws \InvalidArgumentException
    */
    protected function createDriver($driver)
    {
        if (isset($this->customCreators[$driver])) {
            return $this->callCustomCreator($driver);
        } else {
            $method = 'create'.Str::studly($driver).'Driver';

            if (method_exists($this, $method)) {
                // ここでcreateGoogleDriver()が呼び出される
                return $this->$method();
            }
        }

        throw new InvalidArgumentException("Driver [$driver] not supported.");
    }
}

createGoogleDriver()

createGoogleDriverメソッドではconfigファイル(services.google)に設定した情報を取得し、GoogleProviderクラスのインスタンスを生成しています。

SocialiteManager.php
namespace Laravel\Socialite;

class SocialiteManager extends Manager implements Contracts\Factory
{
    /**
    * Create an instance of the specified driver.
    *
    * @return \Laravel\Socialite\Two\AbstractProvider
    */
    protected function createGoogleDriver()
    {
        $config = $this->config->get('services.google');

        return $this->buildProvider(
        GoogleProvider::class, $config
        );
    }
}

AbstractProviderを継承するGoogleProviderインスタンスを生成するまでがSocialite::driver('google')の動きです。

Socialite::driver('google')->redirect()

redirectメソッドでは生成されたリダイレクトURLにリダイレクトするようにRedirectResponseクラスのインスタンスを生成している。

AbstractProvider.php
namespace Laravel\Socialite\Two;

abstract class AbstractProvider implements ProviderContract
{
    /**
     * Redirect the user of the application to the provider's authentication screen.
     *
     * @return \Illuminate\Http\RedirectResponse
     */
    public function redirect()
    {
        $state = null;

        if ($this->usesState()) {
            $this->request->session()->put('state', $state = $this->getState());
        }

        // ここでRedirectResponseクラスのインスタンスを生成
        return new RedirectResponse($this->getAuthUrl($state));
    }



}

getAuthUrl()

getAuthUrlメソッドはGoogleProviderで定義されている

GoogleProvider.php
namespace Laravel\Socialite\Two;

use Illuminate\Support\Arr;

class GoogleProvider extends AbstractProvider implements ProviderInterface
{
     /**
     * {@inheritdoc}
     */
    protected function getAuthUrl($state)
    {
        return $this->buildAuthUrlFromBase('https://accounts.google.com/o/oauth2/auth', $state);
    }
}

AbstractProviderに戻ってURLがどのように生成されるか見ていく。
GoogleのベースURLにクエリパラメータとして認可に必要な各項目をセットしている。

AbstractProvider.php
    /**
     * Build the authentication URL for the provider from the given base URL.
     *
     * @param  string  $url
     * @param  string  $state
     * @return string
     */
    protected function buildAuthUrlFromBase($url, $state)
    {
        // $this->encodingTypeはPHP_QUERY_RFC1738が定義されている
        return $url.'?'.http_build_query($this->getCodeFields($state), '', '&', $this->encodingType);
    }

    /**
     * Get the GET parameters for the code request.
     *
     * @param  string|null  $state
     * @return array
     */
    protected function getCodeFields($state = null)
    {
        $fields = [
            'client_id' => $this->clientId,
            'redirect_uri' => $this->redirectUrl,
            'scope' => $this->formatScopes($this->getScopes(), $this->scopeSeparator),
            'response_type' => 'code',
        ];

        if ($this->usesState()) {
            $fields['state'] = $state;
        }

        return array_merge($fields, $this->parameters);
    }

    /**
     * Format the given scopes.
     *
     * @param  array  $scopes
     * @param  string  $scopeSeparator
     * @return string
     */
    protected function formatScopes(array $scopes, $scopeSeparator)
    {
        return implode($scopeSeparator, $scopes);
    }
}


<おまけ>認可リクエストのカスタマイズ

Laravel公式にもあるようにパラメータや、スコープの追加をすることができる。

パラメータの追加

パラメータを追加するにはwithメソッドを使用します。
$parametersの規定値は空の配列[]です。

AbstractProvider.php
    /**
     * Set the custom parameters of the request.
     *
     * @param  array  $parameters
     * @return $this
     */
    public function with(array $parameters)
    {
        $this->parameters = $parameters;
        return $this;
    }

scopeの追加

scopeに規定値は['openid','profile','email']が定義されている。
追加するには下記のようにscopesメソッドを呼び出すか、setScopeメソッドで上書きするとよい。

SocialOAuthController.php
/**
 * Googleアカウントの認証ページへユーザーをリダイレクト
 */
public function redirectToGoogle()
{
    return Socialite::driver('google')->scopes(['read:user', 'public_repo'])->redirect();
}

メソッドの中身は下記のように配列がマージされる。
array_uniqueメソッドでは、重複した要素が存在する場合、はじめの要素のキーと値が保持されます。
https://www.php.net/manual/ja/function.array-unique.php

AbstractProvider.php
    /**
     * Merge the scopes of the requested access.
     *
     * @param  array|string  $scopes
     * @return $this
     */
    public function scopes($scopes)
    {
        // 
        $this->scopes = array_unique(array_merge($this->scopes, (array) $scopes));
        return $this;
    }

    /**
     * Set the scopes of the requested access.
     *
     * @param  array|string  $scopes
     * @return $this
     */
    public function setScopes($scopes)
    {
        $this->scopes = array_unique((array) $scopes);
        return $this;
    }

リダイレクトURLの変更

リダイレクトURLが複数存在し、configファイルではなく動的に値を変更したい場合はredirectURLメソッドを使用します。

SocialOAuthController.php
/**
 * Googleアカウントの認証ページへユーザーをリダイレクト
 */
public function redirectToGoogle()
{
    return Socialite::driver('google')->redirectURL($redirectUrl)->redirect();
}

AbstractProvider.php
    /**
     * Set the redirect URL.
     *
     * @param  string  $url
     * @return $this
     */
    public function redirectUrl($url)
    {
        $this->redirectUrl = $url;

        return $this;
    }

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

PHPの入門書1周するために、サクッとDockerでApache + MySQL + PHP + phpmyadminの環境つくる

PHP入門書を1周したいので、LAMP環境がほしい

PHP力を上げたいので、入門書からやろうと思い立ちました。
そのためにサクッと用意できて、使いまわせるローカル環境が欲しかったので、そのメモです。

今回はdocker-composeを使って環境構築します。

現在の環境

  • OS:MacOS Big Sur
  • Docker:19.03.13
  • DockerCompose:1.27.4

ディレクトリ構成&ファイル

ディレクトリ構成

.
├── docker-compose.yml
├── html
│   └── index.php
├── mysql
├── php
│   ├── dockerfile
│   └── php.ini
└── phpmyadmin

docker-comopse.yml

version: '3'

services:
  php:
    build:
      context: ./php
      dockerfile: dockerfile
    volumes:
      - ./php/php.ini:/usr/local/etc/php/php.ini
      - ./html:/var/www/html
    ports:
      - 8080:80
  mysql:
    image: mysql:5.7
    volumes:
      - ./mysql:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=root
      - MYSQL_DATABASE=test
      - MYSQL_USER=root
      - MYSQL_PASSWORD=root
  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    environment:
      - PMA_ARBITRARY=1
      - PMA_HOST=mysql
#      - PMA_USER=test
#      - PMA_PASSWORD=test
    links:
      - mysql
    ports:
      - 4040:80
    volumes:
      - ./phpmyadmin/sessions:/sessions

dockerfile

FROM php:7.2-apache
RUN apt-get update && \
  docker-php-ext-install pdo_mysql mysqli mbstring

index.php

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>php7.2-apache</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<?php phpinfo(); ?>
</body>
</html>

php.ini

[Date]
date.timezone = "Asia/Tokyo"
[mbstring]
mbstring.internal_encoding = "UTF-8"
mbstring.language = "Japanese"

エラーすべて表示させたいときは、以下を追加する

error_reporting = E_ALL

実行コマンド

docker-compose.ymlがあるディレクトリに移動して、以下を実行

$ docker-compose up -d

もし、dockerfileを変更した場合は

$ docker-compose up -d --build

php.iniなどの変更を反映させたい時は

$ docker-compose restart

動作確認

phpinfoを表示
http://localhost:8080/
image.png

phpmyadminを表示
http://localhost:4040/
image.png
ログイン画面でdocker-compose.ymlのmysqlコンテナの情報を記載してログインできればOK
image.png

つまづいたところ

  • 当初はdocker-compose.ymlimage: php:7.2-apacheと記載していたが、MySQLのドライバががなくてMySQL動かせなかったので、dockerflieを作成した
  • docker-compose.ymlのphpmyadminコンテナのPMA_USERPMA_PASSWORDを記載すると、phpmyadminにうまくログインできず、データベースが作成できなかったので、コメントアウト

今回やった、やる予定のPHP入門書

PHP入門 確認画面付きのお問い合わせフォームをつくりながらPHPを学ぶ(第2版)
https://amzn.to/2Irz7Fr
※上記環境で動作確認済み

よくわかるPHPの教科書 【PHP7対応版】
https://amzn.to/2LhQ4mW
※まだ未着手

参考記事

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

PHPでクラスを作成する

PHPでクラスを作成

以下の通り、PHPのクラスのコードを書いていく

<?php
class Human{     

⇦クラス名を定義する。クラス名はPHPでは慣例的に大文字で記載する。

public $name;
public $gender;

⇦プロパティ(クラス内で定義された変数)を記載。

public function __construct($name,$gender){
     $this->name = $name;
     $this->gender = $gender;
}
}

コンストラクタはクラスからオブジェクトがnewによって作成される時に自動的に呼び出されるメソッド。
←メソッドはクラス内で定義された関数のこと。

$human = new Human('suzuki','female');
echo $human->name;
echo $human->gender;
?>

$humanをsuzuki,femaleの名前でオブジェクトを作る(インスタンス化)
echoで名前と性別を出力。

出力結果

xxxxxx@xxxxxx test-php % php class.php
suzukifemale
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelですでにあるマイグレーションファイルを書き換えてテーブル定義を変更する

「テーブル作成のマイグレーションファイルに外部キー制約書き忘れた!」みたいなときの対処法です。
マイグレーションファイルが増えるのが気にならない場合や、複数人で管理している場合は、新しくマイグレーションを追加してテーブル定義を変更したほうがいいと思います。

1. テーブルを出力する

対象のデータベースのテーブルを全て出力します。

$ mysqldump --no-create-info --ignore-table=database_name.migrations -u user_name -ppassword > ~/dumps/database_name_20210101.dump

ポイント

  • --no-create-infoオプション : マイグレーションでテーブルは作成するので、CREATE TABLE ~を出力しないオプションです。INSERT INTO ~のみが出力されます。
  • --ignore-table=database_name.migrations : マイグレーションデータが保存されているテーブルを出力しません。

2. マイグレートする

ファイルを書き換えたあと、マイグレーションを実行します。

php artisan migrate:fresh

注意
migrate:freshはデータベース内の全てのテーブルをDropするので、データベースをLaravelアプリケーション以外と共有している場合はmigrate:refresh等を検討してください。

3. 挿入する

出力したデータを再度データベースに追加します。変更したテーブル定義が、変更前と互換性がないと、挿入できないので注意してください。

mysql -u user_name -ppassword -D database_name < ~/dumps/database_name_20210101.dump

エラーがでなければ、大丈夫です。

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

CentOS 8にPHP 7.4, PHP-FPM 7.4をインストール(AppStream)

はじめに

Application Stream(AppStream)を利用してCentOS8にPHP7.4をインストール
親記事:PHP, PHP-FPMの各種インストール方法とEOLまとめ
参考:RHEL8のパッケージ構成 - BaseOSとApplication Stream - 赤帽エンジニアブログ
   第4章 新機能 Red Hat Enterprise Linux 8 | Red Hat Customer Portal

サポート

本手法で導入した場合、Red Hat Enterprise Linux 8 Application Streams Life Cycle - Red Hat Customer Portalより、2029-05がEOLだと思われる。
それ以降に報告された脆弱性や不具合への対応は実施されない可能性がある。

LOG

インストール

# cat /etc/redhat-release
CentOS Linux release 8.3.2011

# yum module install php:7.4
... 略

各種確認

# which php
/usr/bin/php

# php -v
PHP 7.4.6 (cli) (built: May 12 2020 08:09:15) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies

# php -i | grep php.ini
Configuration File (php.ini) Path => /etc
Loaded Configuration File => /etc/php.ini

# which php-fpm
/usr/sbin/php-fpm

# /usr/sbin/php-fpm -v
PHP 7.4.6 (fpm-fcgi) (built: May 12 2020 08:09:15)
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ローカル開発でSymfony CLIがすごい

Symfony Advent Calendar 2020 12日目の記事です。

Symfony CLIはSymfonyローカルマシンでの開発を支援するコマンドラインツールです。Laravel Installer(laravelコマンド)のようにSymfonyプロジェクトの作成にもできますが、それ以外にも優れた機能を備えています。その機能のうち、ぼくがよく使っている便利な機能をいくつかご紹介します。

Symfonyプロジェクトの作成

Symfonyのプロジェクトを作成するにはlavael Installerと同じ感じです。

symfony new [プロジェクト名]
cd [プロジェクト名]

プロジェクト名でディレクトリを作成し、ディレクトリ内にSymfonyをインストールします。以降のコマンドはプロジェクトのディレクトリに移動して、実行します。
余談ですが、Symfony CLIはコマンド実行時に、必ずアップデートを確認・ダウンロードし、常に最新のものが利用できるようになっています。

PHPやSymfony ConsoleなどPHPコマンドを実行する

Symfony CLI経由でPHPなどのコマンドを実行します。これは、後述するバージョン指定、DockerCompose連携で大化けします。

# PHP実行
symfony php -v

PHP 7.4.11 (cli) (built: Oct  1 2020 23:30:54) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
    with Xdebug v2.9.6, Copyright (c) 2002-2020, by Derick Rethans
    with Zend OPcache v7.4.11, Copyright (c), by Zend Technologies

# Symfony Console実行(symfony bin/consoleではないので注意)
symfony console -V
Symfony 5.2.0 (env: dev, debug: true)

# composer実行
symfony composer require twig

その他、pecl, pear, php-fpm, php-cgi, php-config, phpdbg, phpizeが実行できます。

ローカルWebサーバ起動

今まではSymfony Console bin/console でWebサーバを起動していましたが、それに代わってSymfony CLIで起動するようになりました。

# Webサーバ起動
symfony server:start

# バックグラウンドで実行する場合
symfony server:start -d

# Webサーバ終了(バックグラウンド実行時)
symfony server:stop

# ドキュメントルートを変える場合
symfony server:start --document-root=webroot

# ブラウザを開く
symfony open:local

# ログを確認する
symfony server:log

ここでポイントはドキュメントルートを指定できる点。当然ながら、LaravelやCakePHPのプロジェクトでも実行できます。
また、PHP-FPMが有効な場合は、自動でPHP-FPMを利用します。

ローカルをHTTPSにする

php -Sでビルトインサーバを起動した場合、HTTPでの通信になります。上記のSymfony CLIでのローカルWebサーバ起動も同じくHTTPですが、HTTPSに変えることができます。

symfony server:ca:install

このコマンドで、ローカルに証明書をインストールします。以降、サーバを起動するとこのローカル証明書を利用したHTTPS通信になります。インストール後にHTTP通信したい場合は、ローカルWebサーバ起動時に、--allow-httpオプションを指定すると、HTTPとなります。

PHPのバージョンを指定する

プロジェクトによってPHPのバージョンが異なる場合、phpenvでバージョンを切り替えたりしていましたが、プロジェクトディレクトリのルートに、.php-versionという名前のファイルを作成しファイル内にPHPのバージョンを記載すると、Homebrewなどでインストールしたローカルの該当バージョンPHPを、切り替えなしで利用できます。
※該当のバージョンのPHPがインストールされている必要があります

.php-version
8.0

このファイルがある状態でSymfony CLIを実行すると、指定されたバージョンで実行されます。

# ローカルのデフォルトPHPを実行
php -v

PHP 7.4.11 (cli) (built: Oct  1 2020 23:30:54) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
    with Xdebug v2.9.6, Copyright (c) 2002-2020, by Derick Rethans
    with Zend OPcache v7.4.11, Copyright (c), by Zend Technologies

# Symfony CLI経由で、.php-versionに8.0を指定して実行
symfony php -v

PHP 8.0.0-dev (cli) (built: Oct  5 2020 01:14:40) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.0-dev, Copyright (c) Zend Technologies
    with Zend OPcache v8.0.0-dev, Copyright (c), by Zend Technologies

また、composerも指定したPHPのバージョンで実行することができます。

# Symfony CLI経由で、バージョンを指定してcomposer実行
symfony composer require twig

ローカルWebサーバも、もちろん変わります。ただし、ローカルWebサーバ起動中にバージョンを変えても反映されないので、サーバの再起動が必要です。

symfony server:start -d
echo 8.0 > .php-version
symfony server:stop
symfony server:start -d

Screen Shot 2020-12-11 at 11.45.54.png

Screen Shot 2020-12-11 at 11.42.26.png

インストールされているPHPのバージョンを確認する

ローカルにどのPHPのバージョンがインストールされているかsymfony local:php:listで確認することができます。

symfony local:php:list

┌─────────┬──────────────────────────────────┬─────────┬────────────────────────────────┬───────────────────────────────┬─────────┬─────────┐
│ Version │            Directory             │ PHP CLI │            PHP FPM             │            PHP CGI            │ Server  │ System? │
├─────────┼──────────────────────────────────┼─────────┼────────────────────────────────┼───────────────────────────────┼─────────┼─────────┤
│ 7.2.33  │ /usr/local/Cellar/php@7.2/7.2.33 │ bin/php │ sbin/php-fpm                   │ bin/php-cgi                   │ PHP FPM │         │
│ 7.3.11  │ /usr                             │ bin/php │ sbin/php-fpm                   │                               │ PHP FPM │         │
│ 7.4.11  │ /usr/local                       │ bin/php │ Cellar/php/7.4.11/sbin/php-fpm │ Cellar/php/7.4.11/bin/php-cgi │ PHP FPM │ *       │
│ 8.0.0   │ /usr/local/Cellar/php@8.0/8.0.0  │ bin/php │ sbin/php-fpm                   │ bin/php-cgi                   │ PHP FPM │         │
└─────────┴──────────────────────────────────┴─────────┴────────────────────────────────┴───────────────────────────────┴─────────┴─────────┘

The current PHP version is selected from default version in $PATH

Docker Composeと連携する

プロジェクトルートにdocker-compose.ymlがあり、コンテナを起動している場合は、Docker Composeと連携します。前述の状態でSymfony CLIを実行すると、利用しているイメージに合わせて自動的に環境変数が設定されます。
例えば、以下のようなdocker-compose.ymlがある場合、

docker-compose.yml
version: '3'
services:
  database:
    image: mariadb
    ports: [3306]
    environment:
      - MYSQL_ROOT_PASSWORD=root
      - MYSQL_DATABASE=database
      - MYSQL_USER=user
      - MYSQL_PASSWORD=password
  mailer:
    image: mailhog/mailhog
    ports: [8025, 1025]

Dockerを起動して、Symfony CLIでコマンド実行を行うと、docker-compose.yml内の設定(サービス名、イメージ、ポート)を利用して、自動で環境変数を生成します。

docker-compose up -d
symfony server:start -d
symfony var:export --multiline

export DATABASE_DATABASE=database
export DATABASE_DRIVER=mysql
export DATABASE_HOST=127.0.0.1
export DATABASE_NAME=database
export DATABASE_PASSWORD=password
export DATABASE_PORT=49195
export DATABASE_SERVER=mysql://127.0.0.1:49195
export DATABASE_URL=mysql://user:password@127.0.0.1:49195/database?sslmode=disable&charset=utf8mb4
export DATABASE_USER=user
export DATABASE_USERNAME=user
export MAILER_AUTH_MODE=
export MAILER_CATCHER=1
export MAILER_DRIVER=smtp
export MAILER_DSN=smtp://127.0.0.1:49197
export MAILER_HOST=127.0.0.1
export MAILER_PASSWORD=
export MAILER_PORT=49197
export MAILER_URL=smtp://127.0.0.1:49197
export MAILER_USERNAME=
export MAILER_WEB_HOST=127.0.0.1
export MAILER_WEB_IP=127.0.0.1
export MAILER_WEB_PORT=49196
export MAILER_WEB_SCHEME=http
export MAILER_WEB_SERVER=http://127.0.0.1:49196
export MAILER_WEB_URL=http://127.0.0.1:49196
[以下省略]

イメージにあったポートを指定していれば、上記が自動で生成され、.envに記述がなくても上記の環境変数で動作します。また、.envに該当の変数が定義されていても、こちらが優先されます。

docker-compose.ymlのサービス名は、環境変数のプレフィクスになります。サービス名がdatabaseであれば、DATABASE_に、dbであればDB_となります。現在サポートされているサービスは以下の通りです。

サービス ポート Symfonyで使う場合のデフォルトのプレフィクス
MySQL 3306 DATABASE_
PostgreSQL 5432 DATABASE_
Redis 6379 REDIS_
Memcached 11211 MEMCACHED_
RabbitMQ 5672 RABBITMQ_
Elasticsearch 9200 ELASTICSEARCH_
MongoDB 27017 MONGODB_
Kafka 9092 KAFKA_
MailCatcher 1025/1080 or 25/80 MAILER_
MailHog 1025/1080 or 25/80 MAILER_
Blackfire 8707 BLACKFIRE_

※公式にMailHogは記載されてないけど、動く。

Docker Composeと連携している場合は、追加コマンドが利用できるようになります。

# MailHog, MailCatcherを設定している場合は、メールボックスをブラウザで開く
symfony open:local:webmail

# RabbitMQを設定している場合は、マネージャーをブラウザで開く
symfony open:local:rabbitmq

1点注意が必要ですが、Docker ComposeでWebサーバやPHPを定義している場合に、Symfony CLIコマンドをローカルで使ってもコンテナ上のWebサーバやPHPは利用しません。 Symfony CLIは、あくまでローカルでPHPを実行するすることを想定していて、コンテナ上で実行することを想定していません。

"Why would you want to use the web server in a Docker container?"
by Fabien Potencier

※ファビアンも、なんでコンテナなん?って言ってる。

まとめ

他にも機能はありますが、利用する機会が多いのは上記に挙げたものかなと思います。前述の通り、Symfonyだけではなく、LaravelやCakePHP、頑張ればWordPressなどのCMSも動くので、ぜひ一度試してもらえたらと思います。公式ドキュメントはこちら

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

PHP製デプロイツール Deployer v7beta を試す init編

はじめに

  • PHP製のデプロイツールである Deployer が7系になって書き方が結構変わりそうなので触っておく
  • Deployまでやろうと思ったが調査等に時間かけすぎたので一旦アウトプット
  • 本当にさわりだけ、host周りの移行が出来たらまた書く

環境

  • PHP 8.0
  • Deployer 7.0.0-beta.11

初期化

  • dep initで生成されるファイルがphpかyamlか選べるようになっている
  • deploy.yamlとdeploy.php両方ある場合はphpのみ読み込まれる

dep initの実行

$ dep init

            ╔╦╗┌─┐┌─┐┬  ┌─┐┬ ┬┌─┐┬─┐
             ║║├┤ ├─┘│  │ │└┬┘├┤ ├┬┘
            ═╩╝└─┘┴  ┴─┘└─┘ ┴ └─┘┴└─

 ███████████████████████████████████████████████

 Welcome to the Deployer config generator.

 Press ^C at any time to quit.

 Select recipe language [php]:
  [0] php
  [1] yaml
 > 1

 Select project template [common]:
  [0 ] cakephp
  [1 ] codeigniter
  [2 ] common
  [3 ] composer
  [4 ] drupal7
  [5 ] drupal8
  [6 ] flow_framework
  [7 ] fuelphp
  [8 ] joomla
  [9 ] laravel
  [10] magento
  [11] magento2
  [12] prestashop
  [13] provision
  [14] shopware
  [15] silverstripe
  [16] sulu
  [17] symfony
  [18] typo3
  [19] wordpress
  [20] yii
  [21] zend_framework
 > 2

 Repository []:
 >

 Project name [src]:
 > DeployerSample

 Hosts (comma separated) []:
 > example

Successfully created deploy.yaml

生成されたyaml

deploy.yaml
import:
    - recipe/common.php
    - recipe/provision.php

config:
  application: 'DeployerSample'
  repository: ''
  shared_files:
    - .env
  shared_dirs:
    - uploads
  writable_dirs:
    - uploads

hosts:
  example:
    deploy_path: '~/{{application}}'

tasks:
  build:
    script:
      - 'cd {{release_path}} && npm run build'

after:
  deploy:failed: deploy:unlock

生成されたphp

最初の選択をphpにして実行

deploy.php
<?php
namespace Deployer;

require 'recipe/common.php';
require 'recipe/provision.php';

// Config

set('application', 'DeployerSample');
set('deploy_path', '~/{{application}}');
set('repository', '');

add('shared_files', []);
add('shared_dirs', []);
add('writable_dirs', []);

// Hosts

host('example');

// Tasks

task('build', function () {
    cd('{{release_path}}');
    run('npm run build');
});

after('deploy:failed', 'deploy:unlock');

外部ファイルのimport

ファイルツリー

/path/to/project
├─deployer
│  ├─hoge.php
│  └─hoge.yaml
└─deploy.yaml or deploy.php
deployer/hoge.php
<?php

namespace Deployer;

task('hoge:php', fn() => writeln('PHP'));
deployer/hoge.yaml
tasks:
  hoge:yaml:
    script: "touch ./touch-hoge-yaml"

yamlの場合

  • 今回はテスト用に諸々省いたdeploy.yamlを使う
  • 普通にimportの中にファイルを指定するだけで外部ファイルのタスクを取り扱えるようになる
  • 取り敢えずローカルで実行
deploy.yaml
import:
    - deployer/hoge.php
    - deployer/hoge.yaml

hosts:
  example:
    local: true

deployer/hoge.phpのタスクを実行

$ dep hoge:php example
task hoge:php
[example] PHP

deployer/hoge.yamlのタスクを実行

$ dep hoge:yaml example
task hoge:yaml
$ ls touch-hoge-yaml
touch-hoge-yaml

phpの場合

  • yaml同様簡易化したファイル
  • 結果はyamlと変わらないので割愛
deploy.php
<?php
namespace Deployer;

import('deployer/hoge.php');
import('deployer/hoge.yaml');
localhost('example');

memo

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

Laravelのフォームリクエストで他のパラメータを参照する方法

やりたいこと

いろいろあって、バリデーション時に他の入力値を使ってバリデーションしたい。
(今回は、ユーザー情報を暗号化していたため、他の入力値を元にユーザーを探して、一致するかどうかを確認したかった)

方法

$this->request->get()を使用する。

laravel/app/Http/Requests/HogeRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use App\Rules\BirthdayMatched;

class HogeRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        // getメソッドで入力値を取得する。
        $email = $this->request->get('email');

        return [
            'email' => ['required', 'string', 'email'],
            'birthday' => ['required', 'string', new BirthdayMatched($email)],
        ];
    }

あとはカスタムバリデーションルールのプロパティに代入して使用する。

laravel/app/Rules/BirthdayMatched.php
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;
use App\User;

class BirthdayMatched implements Rule
{
    private $email;

    /**
     * Create a new rule instance.
     *
     * @return void
     */
    public function __construct(string $email)
    {
        $this->email = $email;
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        //ここで$this->emailを使ったバリデーション処理をする。
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return '誕生日が一致しません';
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP8 で Call to undefined function each() になる場合の対処法

PHP8 で each 関数が削除されたため、 Call to undefined function each() というエラーになります。

例えば、以下のような処理がある場合は、 current()next() を使用した処理に書き換えます。

// PHP7 までは each() 関数が使える
while (list($k, $v) = each($array)) {
    echo $k;
    echo $v;
}
// PHP8 以降は each() 関数は使えない
while ($current = current($array)) {
    $k = key($array);
    $v = $current;
    echo $k;
    echo $v;
    next($array);
}

Composer のパッケージで each() が使われている場合

Composer のパッケージで each() 関数が使われている場合は、直接書き換えるわけにはいきませんので、解決するためのパッケージを作成しました。

以下のコマンドで PHP8 でも each() 関数を使えるようになります。

composer require nanasess/php8-compat

本来は、こういった互換プログラムを使用するのはあまりよろしくないので、
1. 対応されるまで待つ
2. each() 関数を使わないよう修正し、 Pull Request する
などの対応をするのが良いと思います。

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

WordPressで新規投稿時のURLをデフォルトで連番化

ワードプレスのテーマtwentytwentyを基盤に、新規投稿を作成した際に記事のURLの末尾をデフォルトで連番にする実装を行ったので、備忘録的にこの記事を残す。

環境情報

PHP:version 7.3.12
WordPress:version 5.5.3
WPテーマ:twentytwenty

作業

投稿ページや固定ページのパーマリンクは、デフォルトだとタイトルに入力した文字がそのまま入力されます。
URLに日本語が含まれるとエンコードされてごちゃごちゃしてしまいます。
記事のパーマリンクをデフォルトで連番にしてすっきりさせましょう。

まずは、パーマリンク設定で「カスタム構造」を選択してください。記入欄は %postname% のままで大丈夫です。
次に、functions.php に、下記コードを任意の場所に追記します。

functions.php
// スラッグ連番付番
function incliment_slug( $slug ) {
    global $post;
    // 投稿ページのみ
    if ( ( $slug ) || ( $post->post_type == 'page' ) ) {
        return $slug;
    }
    global $wpdb;
    $slug = $wpdb->get_var( "select max( cast( post_name as unsigned ) ) from { $wpdb->posts } where post_type = 'post' and post_status not in ( 'object', 'attachment', 'inherit' ) and post_name regexp '^[0-9]+$'" );
    $slug ++;
    return $slug;
}
// フィルターフック
add_filter( 'editable_slug', 'incliment_slug' );

wp_nomiss_11.png
新規投稿をしようとするとパーマリンクに1と表示されているのがわかります。

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

【PHP】本番環境と開発環境が同じドメイン内で構築されている場合のファイルパス分岐処理

はじめに

フロント実装者でもときどきPHPを用いた実装を担当することがあると思います。
今回は実際にあった例を用いて、対策を共有できればと思います。

今回のケース

・別の制作会社さんがPHPで作成・運営していたサイトを引き継いだ
・同じドメイン内に本番環境(ドメイン直下)と開発環境(例:ドメイン/develop)がある
・構成を完璧には引き継いでいない(もはや分かる人がいないそうな)ので、できるだけ改修せずにローカロリーで対応する

問題点

前述のとおり本番・開発でディレクトリが違うのだが、PHPでパスルート分岐などをしていないので、作業ファイルをまるっとコピペができない。
よって、開発環境で更新作業を行い、OKが出たら差分箇所を漏れなく本番環境用ファイルに反映させなければならない。大変な二度手間だ。やりたくない。ムキー

対応策

結論から書くと、パスルートを下記の記述に統一させることで対応しました。

<?php
// ドキュメントルートとディレクトリ構成を取得
$doc_root = $_SERVER['DOCUMENT_ROOT'];
$dir = explode('/',$_SERVER["REQUEST_URI"]);

// 開発環境(develop)配下のファイルにはパスルートに /develop を付与
if( $dir[1] == 'develop' ) {
    $current_dir = $doc_root . '/develop';
} else {
    $current_dir = $doc_root;
}

これをconfig.php とかにして読み込ませつつ、

<?php require($_SERVER["DOCUMENT_ROOT"] . '/develop/inc/header.php'); ?>

上記のように書かれていたインクルード部分を

<?php require($current_dir.'/inc/header.php'); ?>

と書き換えました。
これにより、記述の差異が無くなるので本番・開発用ファイルを1つに統合でき、二度手間は解消されました。

まとめ

どういう経緯でこのような構成になっていたのかは分かりませんが、設計の段階で運用の面倒さ、リスクはある程度予測できたと思うので、もうひと工夫しておいてくれればゴニョゴニョと思いました。
でもPHPは昔すこしだけ勉強していたが役に立った気がして、それはそれで良かったと思いましたまる

変なところや、こういうやり方あるよーっていうのがあればご教示いただけると大変ありがたいです。

参考にさせていただいたURL

https://www.php.net/manual/ja/function.set-include-path.php
https://webnetamemo.com/coding/php/201803016775

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

WordPressでカテゴリーで記事を絞り込む(管理画面)

ワードプレスのテーマtwentytwentyを基盤に、管理画面の投稿一覧ページにおいて、投稿をカテゴリーで絞り込めるようにする機能の実装を行ったので、備忘録的にこの記事を残す。

環境情報

PHP:version 7.3.12
WordPress:version 5.5.3
WPテーマ:twentytwenty

作業

今回は投稿一覧ページにカテゴリーを表示させて、絞り込みできるようにしようと思います。
下記のfunctions.phpの任意の場所に追記します。

functions.php
function my_add_filter() {
global $post_type;
    if ( 'news' == $post_type ) {
    ?>
    <select name="tax_news">
        <option value="">タクソノミー指定なし</option>
        <?php
        $terms = get_terms( 'tax_news' );
        foreach ( $terms as $term ) { ?>
        <option value="<?php echo $term->slug; ?>" <?php if ( $_GET['tax_news'] == $term->slug ) { print 'selected'; } ?>><?php echo $term->name; ?></option>
        <?php } ?>
    </select>
    <?php
    }
}
add_action( 'restrict_manage_posts', 'my_add_filter' );

wp_nomiss_13.png
こんな感じにカテゴリーが表示され、絞り込みができるようになりました。
newsやtax_newsなどは、カスタム投稿やタクソノミーを作成したときのものを入力してください。

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

WordPressで投稿編集画面をビジュアルエディタにする

ワードプレスのテーマtwentytwentyを基盤に、WordPressの投稿編集画面をビジュアルエディタで編集できるようにする実装を行ったので、備忘録的にこの記事を残す。

環境情報

PHP:version 7.3.12
WordPress:version 5.5.3
WPテーマ:twentytwenty

作業

htmlの知識があまりない人なら、意味不明なコードが書いてあるテキストエディタよりビジュアルエディタの方を使いたいですよね。
wp_nomiss_18.png
ビジュアルエディタで編集すると、WordPressの仕様でpタグやspanタグが自動挿入されますが、静的コーディングの段階でその想定をしていないと大幅に崩れてしまうことがあります。

以下の点に注意してコーディングしましょう。

・記事コンテンツエリア内の要素にはクラスなしでデザインがあたるようにする。
・要素がどんな順番で置かれても余白が不自然にならないようにする。

また、ビジュアルエディタで記事を作成・編集するときに実際のウェブ上のデザインが当たっているようにするととても分かりやすいのでおすすめです。

ビジュアルエディタを実際のサイト上のデザインに近づけるための方法をご紹介いたします。
editor_style.css(エディタ用CSS)に、サイト用のCSSをインポートする方法です。
WP構築時に、サイトのスタイルを記述しているCSSファイルに、ビジュアルエディタ上に適用させるためのクラス「.wp-editor」も追加します。

style.css
.news-block article h2,//実際のサイト用のクラス
.wp-editor h2 {//管理画面用のクラス
  border-bottom: 4px solid #eaaeb1;
  position: relative;
  padding-bottom: 10px;
  font-size: 24px;
  margin: 60px 0 20px;
}
.news-block article h2,//実際のサイト用のクラス
.wp-editor h2:after {//管理画面用のクラス
  position: absolute;
  content: "";
  display: block;
  border-bottom: 4px solid #ca353b;
  bottom: -4px;
  width: 33%;
}

このように管理画面にもCSSが当たるようにクラスを追記したら、そのCSSファイル(以下の例の場合style.css)をeditor_style.cssでインポートします。

editor_style.css
//editor_style.cssに記述
@import url("style.css");

最後にそのeditor_style.cssをfunctions.phpで読み込みます。

functions.php
// editor_style.css読み込み
function add_editor_style_cb() {
  add_editor_style();
}
add_action('admin_init', 'add_editor_style_cb');

これでビジュアルエディタにも共通のスタイルが適用されるようになります。
wp_nomiss_27_2.png
プレビューを見なくてもビジュアルエディタ上でデザインが確認できるので便利ですね。

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

WordPressでバージョンアップの警告バナーを消す

ワードプレスのテーマtwentytwentyを基盤に、管理者画面に表示されるWordPressのバージョンアップを促すバナーを非表示にする実装を行ったので、備忘録的にこの記事を残す。

環境情報

PHP:version 7.3.12
WordPress:version 5.5.3
WPテーマ:twentytwenty

作業

WordPressの新しいバージョンが公開されると、ダッシュボードにこのような通知が出てきます。
wp_nomiss_21.png
「今すぐ更新してください。」と書いてあると、クライアントは「更新しないと何かまずいのかな?」と感じてアップデートしてしまうかもしれません。
そのようなことがないように、ダッシュボードの更新通知は管理者のみに表示させるようにして、他の権限グループでは非表示にしておきましょう。
ユーザー権限の分け方については後述の「ユーザーの権限を分ける」にて紹介しています。

管理者権限以外で更新通知を非表示にするには、以下のコードをfunctions.phpの任意の場所に追記してください。

//更新通知を管理者権限のみに表示
function update_nag_admin_only() {
  if ( ! current_user_can( 'administrator' ) ) {
    remove_action( 'admin_notices', 'update_nag', 3 );
  }
}
add_action( 'admin_init', 'update_nag_admin_only' );

wp_nomiss_22.png
これで管理者権限以外のユーザーには更新通知が届かなくなりました。

詳しくは下記の参考サイトをご覧ください。
管理画面に表示される更新通知を非表示にする

また、WordPressのデフォルトの設定では、メジャーバージョン・マイナーバージョンは自動で更新されないようになっていますが、ビルドバージョンは自動更新となっています。
ビルドバージョンの更新内容は細かな不具合の修正で、バグやセキュリティホールが見つかった場合に修正をあててくれます。ビルドバージョンの自動更新は切らない方がよいでしょう。

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

WordPressで子テーマを作るための準備

ワードプレスのテーマ twentytwenty を基盤に、子テーマを作る実装を行ったので、備忘録的にこの記事を残す。

環境情報

PHP:version 7.3.12
WordPress:version 5.5.3
WPテーマ:twentytwenty

作業

WordPress の公式テーマなどで配布されているオリジナルテーマに独自に機能を追加したり別のテンプレートを追加したい場合には、「子テーマ」という仕組みを利用します。
もし、オリジナルのテーマを直接変更した場合、テーマのアップデートの際にアップデートの仕組み上最新バージョンのテーマのファイルに上書きされてしまい、自分が行った変更が消えてしまいます。
その状況を防ぐため、親テーマを継承した子テーマを作成してカスタマイズを行う手法をとります。
今回は、例として Twenty Twenty のテーマを継承して作成していきます。「twentytwenty_child」という子テーマを作成します。

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

wp_content/themes 以下に twentytwenty_child というディレクトリを作成し、その中に index.php、style.css、functions.php の3つのファイルを作成する。

style.css の編集

子テーマの style.css に以下を記述する。

style.css
/*
Theme Name: Twenty Twenty Child   <---   子テーマの名前
Author: the WordPress team   <---   子テーマの作成者
Template: twentytwenty   <---   親テーマのディレクトリ名
Description: Twenty Twentyの子テーマです。   <---   テーマの説明
Version: 1.0   <---   子テーマのバージョン
*/

index.php の編集

子テーマの index.php は親テーマの index.php のコピペでよいが、必要に応じて変更箇所を編集することができる。

functions.php の編集

親テーマのスタイルシートを読み込むようにするために、wp_enqueue_scripts をフックに、wp_enqueue_style で style.css を読み込むようにします。
以下を functions.php に記述します。

functions.php
add_action( 'wp_enqueue_scripts', 'theme_enqueue_styles' );
function theme_enqueue_styles() {
    wp_enqueue_style( 'parent-style', get_template_directory_uri() . '/style.css' );
    wp_enqueue_style( 'child-style', get_stylesheet_directory_uri() . '/style.css', array( 'parent-style' ) );
}

管理画面から有効化

WordPressの管理画面から「外観」>「テーマ」>「Twenty Twenty Child」が表示されるので、「有効化」をクリックします。

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

WordPressの独自リソースを一元管理する

ワードプレスのテーマtwentytwentyを基盤に、wp_head( )内の独自のスクリプト、スタイルシートをfunctions.php内で一元管理する実装を行ったので、備忘録的にこの記事を残す。

環境情報

PHP:version 7.3.12
WordPress:version 5.5.3
WPテーマ:twentytwenty

作業

functions.php内に以下のコードを記述する。

functions.php
function my_enqueue_scripts() {
    wp_enqueue_script( 'jquery' );
    wp_enqueue_script( 'bundle_js', get_template_directory_uri() . '/assets/js/bundle.js', array() );
    wp_enqueue_style( 'my_styles', get_template_directory_uri() . '/assets/css/styles.css', array() );
}
add_action( 'wp_enqueue_scripts', 'my_enqueue_scripts' );

その後、header.php内の以下の3行を削除する。

header.php
<link rel="stylesheet" type="text/css" href="<?php echo get_template_directory_uri(); ?>/assets/css/styles.css" />
<script type="text/javascript" src="<?php echo get_template_directory_uri(); ?>/assets/js/jquery-3.3.1.min.js"></script>
<script type="text/javascript" src="<?php echo get_template_directory_uri(); ?>/assets/js/bundle.js"></script>

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

WordPressテーマ開発で最低限必要なファイル

下記の方向け
・WordPressテーマ開発したい
・取りあえす表示させたい
・php初心者

WordPressテーマ開発で最低限必要なファイル

では、WordPressでテーマをアップロードしたとき最低限必要なファイル(構成)についてです。

今回、紹介するのは
WordPressの設定でテーマとして認識されるための構成です。

次のファイルを同じ階層にまとめて、アップロードすれば
とりあえず動くようになります。

index.php

中身は何でも可能です。
ひとまず、「Hello World」的な文言でも書いておきます。

HTMLのベタ書きでもOKです。

style.css

style.css
/*
Theme Name: サイトorブログ名等々
*/

他に、記載しとくと良いものも合わせると
下のようになります。

style.css
/*
Theme Name: サイトorブログ名等々
Description: テーマの説明
Version: 1.0.0
Author:  テーマの作成者
*/

まとめ

以上です。
あとは上の2つのファイルを1つのファイルにまとめてWordPressにアップロードして確認してみて下さい。

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

Laravel8で完成されたModelFactoryの使い方

Laravel Advent Calendar 2020 - Qiita の 8日目 の記事です。
昨日は @okdyy75 さんのLaravelを触って1年経ったのでTIPSの記事でした!
明日は @kaino5454 さんの記事です!

概要

Laravel8からModelFactoryの機能が刷新されました。
新しい書き方についてまとめたいと思います。

こちらの記事をリスペクトしてタイトル名に使わせていただきました???

予備知識

Laravelのデータベース関連の用語として、Migration、Seeder、ModelFactory、Fakerの4つあります。

Migration(マイグレーション)

https://readouble.com/laravel/8.x/ja/migrations.html

マイグレーションはデータベースのテーブルの定義を書きます。
英語で「移動・移行」という意味になります。

テーブルの変更のソースコードで管理することができます。
そのため本番環境へリリースしたり、新しいメンバーが参画した際にも簡単に同じテーブルの状態を再現できます。

artisan コマンド使用例

$ php artisan make:migration create_posts_table # マイグレーションのテンプレート生成
$ php artisan migrate # マイグレーション実行
$ php artisan migrate:fresh # 全テーブル削除、全マイグレーション再実行
$ php artisan migrate:fresh --seed # シーディングも行う

同じ名前のマイグレーションファイルはエラーになるので、注意して命名していく必要があります。
1ファイルで複数テーブルのマイグレーション定義は書かないほうが良いです。

Seeder(シーダー)

https://readouble.com/laravel/8.x/ja/seeding.html

シーダーはデータベースにダミーデータを一斉に挿入できる機能です。
英語で「種をまく人」という意味になります。(Seeding/シーディングとも表記されます)

artisan コマンド使用例

$ php artisan make:seeder PostsSeeder # シーダーのテンプレート生成
$ php artisan db:seed # DatabaseSeeder を実行
$ php artisan db:seed --class=PostsSeeder # クラス指定して実行

テーブル単位やデータのまとまり毎に書くと良いでしょう。

ModelFactory(モデルファクトリ)

https://readouble.com/laravel/8.x/ja/database-testing.html#creating-factories

モデルファクトリはEloquentモデルの各フィールドに入る値を定義します。
モデルの工場といったところです。

主にシーダーやテストコードからモデルファクトリーは呼び出されます。

artisan コマンド使用例

$ php artisan make:factory PostFactory # モデルファクトリのテンプレート生成

Facker(フェイカー)

フェイカーはダミーデータ生成用のライブラリです。
主にモデルファクトリを定義する際に使用します。

名前や住所、メールアドレスといったよく使われるテストデータをランダムに生成してくれる便利な機能です。

補足: fzaninotto/Fakerのアーカイブ

最近の話(2020年10月21日)ですが、 fzaninotto/Faker の本家のリポジトリがアーカイブされています。
詳細については、下記のリンクをご参照ください。

今後はフォーク先のリポジトリでメンテナンスが続けられるそうです。
また、こちらのプルリクエスト で既にライブラリの変更が行われてます。

  • v8.12.0 (2020-10-29)
  • v7.29.0 (2020-10-29)
  • v6.20.0 (2020-10-28)

上記のバージョン以降のLaravelをご利用であればフォーク先のリポジトリが使用されます。

tinkerでお試し実行方法

ModelFactory, Fakerはtinker上で簡単にお試しできます。
どんなテストデータが生成されるのか気になる場合は、tinkerで試してみましょう!

ModelFactoryのお試し実行方法

$ php artisan tinker

# make() だとインスタンスのみ返す
>>> App\Models\User::factory()->make()
=> App\Models\User {#3385
     name: "Miss Dolores Mueller III",
     email: "marcus19@example.org",
     email_verified_at: "2020-12-06 14:58:34",
   }

# create() だとデータベースに挿入してインスタンスを返す
>>> App\Models\User::factory()->create()
=> App\Models\User {#3387
     name: "Annabel Upton III",
     email: "kenton.klocko@example.org",
     email_verified_at: "2020-12-06 14:58:39",
     updated_at: "2020-12-06 14:58:39",
     created_at: "2020-12-06 14:58:39",
     id: 1,
   }

Fakerのお試し実行方法

詳しい使い方はこちら https://fakerphp.github.io

$ php artisan tinker
>>> $faker = app()->make(Faker\Generator::class)

>>> $faker->name()
=> "中津川 和也"

# () なしでメソッドではなくプロパティへのアクセスでも実行可能
>>> $faker->name
=> "佐々木 学"

>>> $faker->safeEmail()
=> "kondo.yui@example.com"

>>> $faker->emoji()
=> "?"

>>> $faker->imageUrl()
=> "https://via.placeholder.com/640x480.png/0066ee?text=aut"

>>> $faker->dateTimeBetween('-2weeks', '9 days')->format('Y-m-d H:i:s')
=> "2020-11-28 05:24:51"

設定

Faker 日本語化

config/app.phpfaker_locale の値を変更してください。

config/app.php
    /*
    |--------------------------------------------------------------------------
    | Faker Locale
    |--------------------------------------------------------------------------
    |
    | This locale will be used by the Faker PHP library when generating fake
    | data for your database seeds. For example, this will be used to get
    | localized telephone numbers, street address information and more.
    |
    */

//    'faker_locale' => 'en_US',
    'faker_locale' => 'ja_JP',

実行すると日本語名のデータが入るようになります。

>>> App\Models\User::factory()->make()->name
=> "田中 桃子"

本題

横道に逸れてしまいましたが、ここからが本題のModelFactoryです。

シーダーからテストデータを挿入する

シーダーからテストデータをデータベースに挿入したい場合、こんな感じで生成することができます。

database/seeders/DatabaseSeeder.php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        DB::table('users')->insert([
            'name' => Str::random(10),
            'email' => Str::random(10).'@gmail.com',
            'password' => Hash::make('password'),
        ]);
    }
}

数件程度であれば良いですが、開発が進むにつれて複雑なテストデータを作ろうとしていくとメンテナンスがとても大変になってしまいます...。

デフォルトで用意されているモデル

app/Models/User.php
<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}

Illuminate\Database\Eloquent\Factories\HasFactory trait を use する必要があります。

デフォルトで用意されているモデルファクトリ

database/factories/UserFactory.php
<?php

namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

class UserFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = User::class;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'name' => $this->faker->name,
            'email' => $this->faker->unique()->safeEmail,
            'email_verified_at' => now(),
            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
            'remember_token' => Str::random(10),
        ];
    }
}

tinker からモデルファクトリーを呼び出して使う

$ php artisan tinker

App\Models\User::factory()->create()
=> App\Models\User {#3380
     name: "近藤 結衣",
     email: "uno.naoki@example.net",
     email_verified_at: "2020-12-06 15:43:28",
     updated_at: "2020-12-06 15:43:28",
     created_at: "2020-12-06 15:43:28",
     id: 3,
   }

// name 属性を上書きする
App\Models\User::factory()->create(['name' => 'ucan'])
=> App\Models\User {#4264
     name: "ucan",
     email: "sato.yuki@example.com",
     email_verified_at: "2020-12-06 15:43:46",
     updated_at: "2020-12-06 15:43:46",
     created_at: "2020-12-06 15:43:46",
     id: 4,
   }

Laravel7との違い

database/factories/UserFactory.php
<?php

/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\User;
use Faker\Generator as Faker;
use Illuminate\Support\Str;

/*
|--------------------------------------------------------------------------
| Model Factories
|--------------------------------------------------------------------------
|
| This directory should contain each of the model factory definitions for
| your application. Factories provide a convenient way to generate new
| model instances for testing / seeding your application's database.
|
*/

$factory->define(User::class, function (Faker $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'email_verified_at' => now(),
        'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
        'remember_token' => Str::random(10),
    ];
});

呼び出し方

factory(App\User::class)->create();

以前は $factory->define() 関数を使って登録、 factory() 関数を使って呼び出ししていく形でしたが、Laravel 8からクラスベースとなりました。

基本の流れ

posts テーブル

テーブルがないとテストデータを入れれないので、下記のようにマイグレーションファイルを作っておきます。

$ php artisan make:migration create_posts_table

database/migrations/YYYY_MM_DD_XXXXXX_create_posts_table.php といったマイグレーションファイルが生成されるので、下記のように追記しておきます。

database/migrations/2020_12_08_000000_create_posts_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id');
            $table->string('title');
            $table->text('content');
            $table->string('published_at')->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('posts');
    }
}
$ php artisan migrate

モデル、モデルファクトリークラスの生成

新しく自分でファイルを作成しても良いですが、テンプレートを生成する便利コマンドがあるので使いましょう。

# モデルとファクトリー個別に生成する
$ php artisan make:model Post
$ php artisan make:factory PostFactory --model=Post

or

# モデルとファクトリーを生成する
$ php artisan make:model Post --factory
$ php artisan make:model Post -f # 省略オプション

or

# モデルとファクトリー、マイグレーション、シーダー、コントローラを生成する
$ php artisan make:model Post --all
$ php artisan make:model Post -a # 省略オプション

生成されるファイル

  • app/Models/Post.php
  • database/factories/PostFactory.php
app/Models/Post.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use HasFactory;
}
database/factories/PostFactory.php
<?php

namespace Database\Factories;

use App\Models\Post;
use Illuminate\Database\Eloquent\Factories\Factory;

class PostFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = Post::class;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            //
        ];
    }
}

database/factories/PostFactory.phpdefinition() を書き換えてみます。

database/factories/PostFactory.php
    public function definition()
    {
        return [
            'user_id' => \App\Models\User::factory(),
            'title' => $this->faker->title,
            'content' => $this->faker->paragraph,
        ];
    }

シーダーへ追記します。

database/seeders/DatabaseSeeder.php
    public function run(): void
    {
        \App\Models\Post::factory()->count(3)->create();
    }
$ php artisan migrate:fresh --seed
$ php artisan tinker

>>> App\Models\User::count();
=> 3

>>> App\Models\Post::count();
=> 3

>>> App\Models\User::first();
=> App\Models\User {#4044
     id: 1,
     name: "斉藤 里佳",
     email: "yamamoto.asuka@example.org",
     email_verified_at: "2020-12-06 16:43:25",
     created_at: "2020-12-06 16:43:25",
     updated_at: "2020-12-06 16:43:25",
   }

>>> App\Models\Post::first();
=> App\Models\Post {#3639
     id: 1,
     user_id: 1,
     title: "Dr.",
     content: "Aspernatur maiores qui neque iure cupiditate. Facilis voluptas consequatur mollitia placeat ea. Aperiam neque qui quae placeat debitis nobis minima.",
     published_at: null,
     created_at: "2020-12-06 16:43:25",
     updated_at: "2020-12-06 16:43:25",
   }

Laravelではこんな流れでテストデータを作成していきます。

詳細な使い方

https://readouble.com/laravel/8.x/ja/database-testing.html

公式ドキュメントに詳しい使い方が記載されているので詳細はこちらを参考すると良いです。

補足

公式ドキュメントに書かれてないけど、知ってると便利そうなことをいくつかご紹介します。

Fakerに独自のメソッドを追加する

FakerServiceProvider を作成します。

$ php artisan make:provider FakerServiceProvider

FakerServiceProvider を登録します。

config/app.php
    'providers' => [
        App\Providers\FakerServiceProvider::class,
    ],

FakerServiceProvider を登録します。

app/Providers/FakerServiceProvider.php
<?php declare(strict_types=1);

namespace App\Providers;

use App\Faker\Planet;
use Faker\Generator;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Support\ServiceProvider;

class FakerServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap services.
     *
     * @return void
     * @throws BindingResolutionException
     */
    public function boot(): void
    {
        $faker = $this->app->make(Generator::class);
        $faker->addProvider(new Planet($faker));
    }
}
app/Faker/Planet.php
<?php declare(strict_types=1);

namespace App\Faker;

use Faker\Provider\Base;

/**
 * 惑星をランダムに返す
 */
class Planet extends Base
{
    protected array $planet = [
        '水星',
        '金星',
        '地球',
        '火星',
        '木星',
        '土星',
        '天王星',
        '海王星',
    ];

    /**
     * @return string
     */
    public function planet(): string
    {
        return $this->generator->randomElement($this->planet);
    }
}
$ php artisan tinker
>>> $faker = app()->make(Faker\Generator::class)

>>> $faker->planet()
=> "海王星"

モデルファクトリーのリレーション定義

ファクトリークラスにリレーションの定義も行えます。
ただ私個人としては、ファクトリークラスはなるべくシンプルに保ち、シーダークラスに生成ルールを書いた方が読みやすいかなと思ってます。

連番を生成する

連番のテストデータを作りたい時は、staticなプロパティを作ると良いです。

class UserFactory extends Factory
{
    private static $sequence = 1;

    public function definition()
    {
        return [
            'name' => sprintf('No. %d %s', self::$sequence++, $this->faker->name),
        ];
    }

実行するとこんな感じです。

>>> App\Models\User::factory()->make()->name
=> "No. 1 青山 明美"

>>> App\Models\User::factory()->make()->name
=> "No. 2 工藤 七夏"

>>> App\Models\User::factory()->make()->name
=> "No. 3 渡辺 稔"

$faker->email は使うな! $faker->safeEmail を使え!

$ php artisan tinker
>>> $faker = app()->make(Faker\Generator::class)

>>> $faker->email
=> "kyosuke.sasada@gmail.com"

>>> $faker->safeEmail
=> "kondo.yui@example.com"

実行結果を見るとわかりますが、 @gmail.com など実在するドメインのテストデータが生成されます。
開発環境でメールの誤送信を防止するため $faker->safeEmail を使うようにしましょう。

Faker nullデータをランダムで入れたい

->optional() を使うとランダムでnull値が入ります!

    public function definition()
    {
        return [
            // ランダムでnullを入れたい時
            'completed_at' => $this->faker->boolean ? $this->faker->dateTime : null,
            // Fakerのoptional()を使うとランダムでnullを入れてくれます(オススメ)
            'completed_at' => $this->faker->optional()->dateTime,
            // 80%の確率で日付データが入り、20%の確率でnullが入ります
            'completed_at' => $this->faker->optional(0.8)->dateTime,
        ];
    }

状態に応じたテストデータを作成する

公式サイトに詳しく記述されているので、ここで書く必要ないですがとても便利な機能なのでご紹介します。

※お試しする場合はマイグレーションファイルに下記を追記して、php artisan migrate:fresh してください。

database/migrations/2014_10_12_000000_create_users_table.php
            $table->string('account_status');
database/factories/UserFactory.php
<?php

namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

class UserFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = User::class;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'name' => $this->faker->name,
            'account_status' => 'init',
            'email' => $this->faker->unique()->safeEmail,
            'email_verified_at' => now(),
            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
            'remember_token' => Str::random(10),
        ];
    }

    /**
     * そのユーザーが有効化(activated)されていることを表す
     *
     * @return Factory
     */
    public function activated(): Factory
    {
        return $this->state(function () {
            return [
                'account_status' => 'activated',
            ];
        });
    }

    /**
     * そのユーザーが資格保留(suspended)されていることを表す
     *
     * @return Factory
     */
    public function suspended(): Factory
    {
        return $this->state(function () {
            return [
                'account_status' => 'suspended',
            ];
        });
    }
}
$ php artisan tinker

# 仮登録(init)状態のユーザーを作成
>>> App\Models\User::factory()->create()
=> App\Models\User {#3346
     name: "中津川 あすか",
     account_status: "init",
     email: "tanaka.momoko@example.org",
     email_verified_at: "2020-12-06 18:30:21",
     updated_at: "2020-12-06 18:30:21",
     created_at: "2020-12-06 18:30:21",
     id: 7,
   }

# 有効化(activated)状態のユーザーを作成
>>> App\Models\User::factory()->activated()->create()
=> App\Models\User {#3343
     name: "石田 京助",
     account_status: "activated",
     email: "harada.yuta@example.com",
     email_verified_at: "2020-12-06 18:30:23",
     updated_at: "2020-12-06 18:30:23",
     created_at: "2020-12-06 18:30:23",
     id: 8,
   }

# 資格保留(activated)状態のユーザーを作成
>>> App\Models\User::factory()->suspended()->create()
=> App\Models\User {#4090
     name: "伊藤 稔",
     account_status: "suspended",
     email: "taichi.sugiyama@example.com",
     email_verified_at: "2020-12-06 18:30:24",
     updated_at: "2020-12-06 18:30:24",
     created_at: "2020-12-06 18:30:24",
     id: 9,
   }

モデルの状態によって必要な埋めたいデータは異なるので、状態に応じてモデルファクトリを定義できるのはとても便利です!

db:seed でテストデータをクリアする

先週書いた記事ですが、こちらもやっておくと便利です。

Laravel 8 のリリース記事

Laravel 8の新しい機能を知りたい方はこちらの記事をご覧ください。

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

※学習用メモ デザインパターン:Mediator編[PHP]

Mediatorパターンとはどういうものか

Mediatorとは仲介者という意味です。

プログラムを組んでいく中で、様々なクラスがお互いにやり取りする状態が多くなり、複雑な構造となることがしばしばあります。
そこでオブジェクト間のやり取りを仲介するmediatorを配置することで、オブジェクト同士で直接通信を行うことなくお互いの依存関係を削減することができます。

登場するクラス

Mediator:オブジェクト間のやり取りを仲介するクラス
Colleague:Mediatorによってやり取りされる側のクラス

[参考サイト]
https://ja.wikipedia.org/wiki/Mediator_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3
https://bmf-tech.com/posts/PHP%E3%81%A7%E5%AD%A6%E3%81%B6%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3%20-%20Mediator%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3
https://qiita.com/i-tanaka730/items/9f96d28d32ab3c9099d9#4-2-loginframe%E3%82%AF%E3%83%A9%E3%82%B9
https://www.ritolab.com/entry/137

Mediatorパターンを用いるメリット

・オブジェクト間のやり取りがmediatorによって行われるため、処理がカプセル化される。
・オブジェクト同士の複雑な依存関係を集約できる。

サンプルコード

仕様

・出社しているアルバイトに仕事を割り振る
・料理ができる人は優先的にキッチンへ
・シフトがいっぱいになったら出社できないように

アルバイトのクラス(Colleague)
クラス内でのちに登場するMediatorクラスを保持しています。
arriveAtWork(出社)メソッドを呼ぶことで、Mediator側にWokerクラスの状態判断を委ねます。
つまり、このクラス内で他のオブジェクトとの関係性を気にする必要がなくなるという事です。

Worker.class.php
class Worker {
    private $name = "";
    private $goodCookFlg = 0;
    private $workerMediator = null;

    public function __construct($name, $goodCookFlg)
    {
        $this->name = $name;
        $this->goodCookFlg = $goodCookFlg;
    }

    public function setWorkerMediator($workerMediator)
    {
        $this->workerMediator = $workerMediator;
    }

    public function getRole()
    {
        return $this->workerMediator->getRole($this->name);
    }

    public function getName()
    {
        return $this->name;
    }

    public function getGoodCookFlg()
    {
        return $this->goodCookFlg;
    }
}

Mediatorクラス
このMediatorクラスで各Wokerクラスの状態を判断し結果を指定しています。
(コンパクトさに欠ける形となってしまいましたが、、^^;)
先にも記述したように、Mediator側で各クラスの状態に応じた判断を行うため、
Wokerクラス内で他のオブジェクトとの関係性を気にせず、すべての判断をこのMediatorクラス内に集約できます。

WorkerMediator.class.php
class WorkerMediator {

    const KITCHEN = 1;
    const FLOOR = 2;
    const WORKER_CAPACITY = 4;

    private $workersInfo = array();

    public function arriveAtWork($worker)
    {
        $worker->setWorkerMediator($this);
        $this->divisionOfRoles($worker->getName(), $worker->getGoodCookFlg());
    }

    // 他オブジェクトの状態に応じて役割分担
    private function divisionOfRoles($name, $goodCookFlg)
    {
        if (count($this->workersInfo) >= self::WORKER_CAPACITY) {
            return 0;
        }

        if (count($this->workersInfo) == 0) {
            if ($goodCookFlg) {
                $this->workersInfo[$name] = self::KITCHEN;
            } else {
                $this->workersInfo[$name] = self::FLOOR;
            }
        } else {
            $counts = array_count_values($this->workersInfo);
            if ($goodCookFlg) {
                if ($counts[self::KITCHEN] >= self::WORKER_CAPACITY - 1) {
                    $kitchenArray = array_keys($this->workersInfo, self::KITCHEN);
                    $this->workersInfo[$kitchenArray[0]] = self::FLOOR;
                    $this->workersInfo[$name] = self::KITCHEN;
                } else {
                    $this->workersInfo[$name] = self::KITCHEN;
                }
            } else {
                if (!in_array(self::KITCHEN, $this->workersInfo)) {
                    $this->workersInfo[$name] = self::KITCHEN;
                } elseif (!in_array(self::FLOOR, $this->workersInfo)) {
                    $this->workersInfo[$name] = self::FLOOR;
                } elseif ($counts[self::KITCHEN] > $counts[self::FLOOR]) {
                    $this->workersInfo[$name] = self::FLOOR;
                } else {
                    $this->workersInfo[$name] = self::KITCHEN;
                }
            }
        }
    }

    public function getRole($name)
    {
        if (!array_key_exists($name, $this->workersInfo)) {
            return $name ."さんはシフトがいっぱいなので出社できません。\n";
        } elseif ($this->workersInfo[$name] == self::FLOOR) {
            return $name . "さんはフロアです。\n";
        } else {
            return $name . "さんはキッチンです。\n";
        }
    }
}

使ってみる

5人のアルバイトが順番に出社
料理できる人にはフラグが立っている

$workerMediator = new WorkerMediator();

$tanaka = new Worker('田中', 1);
$sato = new Worker('佐藤', 1);
$kato = new Worker('加藤', 0);
$tsuji = new Worker('辻', 1);
$kusunoki = new Worker('楠木', 1);

// 出社
$workerMediator->arriveAtWork($tanaka);
$workerMediator->arriveAtWork($sato);
$workerMediator->arriveAtWork($kato);
$workerMediator->arriveAtWork($tsuji);
$workerMediator->arriveAtWork($kusunoki);

echo $tanaka->getRole();
echo $sato->getRole();
echo $kato->getRole();
echo $tsuji->getRole();
echo $kusunoki->getRole();


/*結果
田中さんはキッチンです。
佐藤さんはキッチンです。
加藤さんはフロアです。
辻さんはキッチンです。
楠木さんはシフトがいっぱいなので出社できません。
 */

まとめ

・Mediatorパターンは、各Colleagueクラス間での相互関係などによるやり取りをすべて1点に集約することで全体の構造をシンプルに、かつオブジェクト同士の依存関係を削減できる。
・サンプルコードにて、出社したのに入れないのは仕様として問題ありですね。。楠木さんごめんなさい。。

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

Composerパッケージを作ろう その② - Laravelパッケージ編

はじめに

今回はLaravelのComposr自作パッケージの公開手順を書いていきます。
基本的な手順は前回の記事と同様なので、まだご覧になっていない人はこちらの記事から先に読むことをお勧めします。

Composerパッケージを作ろう その①

下準備

ディレクトリやファイルの構成や準備などは前回の記事を参考にしてください。

Laravelでパッケージを自作する際はLaravelというFWのコアとなるサービスプロバイダをパッケージ側にも実装します。

Laravel5.5以降からはPackage Auto Discoveryという仕組みが導入されており、作成するパッケージのcomposer.jsonに以下の様に追記しておくことでサービスプロバイダを自動的に読み込んでくれます。

"extra": {
        "laravel": {
            "providers": [
                "Vendor\\PackageName\\PackageServiceProvider"
            ]
        }
    }

サービスプロバイダ内でルートを生成したり、作成したルートにミドルウェアを挟んだりすれば、デバッグツールの様なものを作成することも可能です。

ちなみに自分は以下の様なパッケージを公開しています。
temori/artisan-browser

ブラウザ上でターミナル風の画面を表示してルートの確認やartisanコマンドを実行するための開発ツールになります。

57972369-476f1280-79d4-11e9-9918-dfddb07a2d80.gif

このパッケージでもサービスプロバイダでルートを生やして、ミドルウェア内でhtmlの要素やjs,cssを表示しているviewに追加する形でツールの画面を表示させています。

基本的な構成は前回の記事と同様ですが、Laravel用にディレクトリを若干追加しています。

以下が作成したパッケージのディレクトリになります。

├── artisan-browser
│   ├── LICENSE
│   ├── README.md
│   ├── composer.json
│   ├── config
│   │   └── artisanbrowser.php
│   └── src
│       ├── ArtisanBrowser.php  #このパッケージの基幹クラス
│       ├── ArtisanBrowserServiceProvider.php  #サービスプロバイダー
│       ├── Controllers  #ルートを生成したので、そこで呼び出すコントローラー
│       │   ├── AjaxController.php
│       │   └── AssetController.php
│       ├── Facades
│       │   └── ArtisanBrowser.php
│       ├── Logs
│       │   └── HistoryLogger.php
│       ├── Middleware  #パッケージのhtml要素を全ページにレンダリングするためのミドルウェア
│       │   └── Inject.php
│       ├── Renderer
│       │   ├── AssetRenderer.php
│       │   └── HtmlRenderer.php
│       ├── Resources
│       │   ├── artisanBrowser.css
│       │   ├── artisanBrowser.js
│       │   ├── image
│       │   │   └── logo.png
│       │   ├── vendor
│       │   │   ├── draggabilly.pkgd.min.js
│       │   │   ├── jquery-1.12.4.min.js
│       │   │   └── suggest.js
│       │   └── views
│       │       └── index.blade.php
│       └── helpers.php

作成するパッケージによってディレクトリを適宜作成してください。

パッケージの処理を実装する

ここも前回の記事と一緒ですが、もし設定ファイル等で挙動を制御する様な場合、
パッケージ使用者に設定ファイルを編集してもらいたい場面が出てくるかと思います。

その様な場合はLaravelにはアプリケーション側にassetを配置する仕組みが存在しているので、それを活用しましょう。

サービスプロバイダのbootメソッド内に

$this->publishes([
            __DIR__ . '/../config/artisanbrowser.php' => config_path('artisanbrowser.php', 'config'),
        ]);

の様な記述をしておくと

# forceオプションはすでにファイルが存在していても上書きでファイルを配置する
php artisan vendor:publish --tag=config [--force]

で設定ファイルがアプリケーション側へ配置されます。

もしくはアプリケーション側とvendor下のパッケージの設定ファイルをマージすることもできます。

registerメソッド内に以下の様に定義すればパッケージ側の設定をデフォルトとして使用することができ、ユーザーは変更したい値のみオーバーライドすればパッケージ側とアプリケーション側の設定値をマージしてくれます。

$this->mergeConfigFrom(
        __DIR__ . '/../config/artisanbrowser.php', 'artisanbrowser'
    );

詳細は公式のパッケージ開発に詳しく書かれています。

パッケージの実装が完了したらその後の手順は前回の記事と同様です。

最後に

2回に分けてComposer自作パッケージの流れを書いていきましたがいかがでしたでしょうか?
ざっくりとしか流れしか書いていませんが、そこまでハードルの高いものではないと少しでも感じてもらえたら嬉しいです:grinning:

手始めに自分が使う用にパッケージを作ってみるのもありかも知れません。

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

Composerパッケージを作ろう その①

はじめに

PHPを使っていてComposerのお世話になっていない人はいないと思いますが、公開されているパッケージを使用するだけではなく、自作のパッケージを公開してやろうという人向けの記事になります。

OSS活動をしていきたいけど具体的な手順はどうなっているんだーと思っている人の一助になれば幸いです:blush:
(※自分もこれまでパッケージ公開などはやってきていなかったので、アドバイス等あれば喜びます)

temori/distancexport
スクリーンショット 2020-11-11 9.30.02.png
上記パッケージはDB間でデータを移行する必要があるテーブル名やカラム名が違うデータの移行の際にスプレッドシート形式で移行先のカラムと移行元のカラムを編集できたら便利だなーと思って作成しました。

最終目標

packagistに公開してcomposer require [パッケージ名]でパッケージがインストールできるまでを目指します。

環境構築

開発環境はいつもdockerで準備しています。
お好きな環境で開発すれば良いかと思います。

ちなみに自分は以下のリポジトリの環境で開発しています。
temori1919/docker-lamp

プロジェクト作成

まずパッケージ用のディレクトリをします。
最低限以下の様なディレクトリ構成でいいかと思います。

自分はpackageディレクトリの下にvendor名パッケージ名でディレクトリを作成していますが、ルートディレクトリがパッケージ名でも問題ないかと思います。

package
└── vendor名
    └── パッケージ名
        ├── LICENSE       # githubのリポジトリ内で作成することが可能です
        ├── README.md     # パッケージの概要を記述するので完成してから作成しても構いません
        ├── composer.json # 後述するcomposerコマンドで生成されるファイルです
        ├── .gitignore    # gitから除外したいファイルがあれば作成します
        ├── src           # パッケージのソースを格納するディレクトリです
        └── tests         # テストコードを格納するディレクトリです(なくてもOK)

ディレクトリを作成したら、composer initコマンドでcomposer.jsonを作成します。

外部パッケージを定義しているいつものあのファイルです。

$ cd path/to/package/vendor名/パッケージ名

# コマンドを実行すると対話形式でファイル作成が始まります
$ composer init

  Welcome to the Composer config generator  

This command will guide you through creating your composer.json config.

# ベンダー名/パッケージ名を入力してEnter(ここで入力したものが外部公開した時のベンダー名/パッケージ名になります)
Package name (<vendor>/<name>) [some/mypackage]:
# パッケージの概要を記述しますが空白でも構いません。(後述するpackagistの概要に表示される箇所になります)
Description []:
# 製作者、nでスキップしても問題ありません
Author [some <some@address.com>, n to skip]:
# 最小安定性、そのままEnterでも問題ありません
# 作成するパッケージの安定性を記述する箇所になります
# デフォルトは「stable」で使用できる値は「dev」、「alpha」、「beta」、「RC」、「stable」のみとなります
Minimum Stability []:
# パッケージのタイプ、そのままEnterでOK
# デフォルトが「library」(vendorの下にインストールされるタイプ)でそのほかに「project」、「metapackage」、「composer-plugin」があります
Package Type (e.g. library, project, metapackage, composer-plugin) []: 
# ライセンスのタイプ(用途にあうライセンスを指定してください)
License []:
# 使用する予定のあるパッケージを検索します
# とりあえず使用する予定はないのでそのままEnter
Would you like to define your dependencies (require) interactively [yes]?
Search for a package:
# こちらは上記のdevパッケージになります
# 使用予定のものがあればここで検索してもいいですが、後から追加でもOKなのでとりあえずEnter
Would you like to define your dev dependencies (require-dev) interactively [yes]?
Search for a package:

# コマンドに記述した内容の確認になります
# 問題なければEnter(あとでファイルを直接修正することもできます)
{
    "name": "some/mypackage",
    "license": "MIT",
    "require": {}
}

Do you confirm generation [yes]? 

パッケージディレクトリの直下にcomposer.jsonができていると思います。

mypackage/composer.json

これでディレクトリの準備ができました。

パッケージの処理を実装する

ディレクトリやダミープロジェクトの作成

srcディレクトリ配下にクラスやメソッドを作成していきます。

パッケージを開発する際はローカルのディレクトリにあるパッケージをcomposer requireして開発を進めていきます。

まず開発するパッケージをインストールするダミーのプロジェクトを作成します。
先ほど作成したパッケージ用ディレクトリと同階層になります。

├── package
└── dummy-project

※FWを使用してのプロジェクト作成でも問題ありません。

作成したプロジェクトに移動してcomposer.jsonを作成し以下を追記します。
※FWを使用してプロジェクトを作成した場合は、すでに配置されているcomposer.jsonに追記します。
sh
cd dummy-project

composer.json
    "repositories": [
        {
            "type": "path",
            "url": "../package/vendor名/パッケージ名"
        }
    ]

これでcomposer initの際に指定したベンダー名/パッケージ名

composer require ベンダー名/パッケージ名

コマンドを打てばローカルのディレクトリがパッケージとしてインストールされます。

composer.jsontypeを変更することにより、インストール先を変更することができます。repositoriesの指定がないと、デフォはpackagistからパッケージを検索します。
pathでローカルディレクトリからの読み込み、vcsでgithub等のリポジトリホスティングサービスから直接インストールなどインストール先を変更することが可能です。

詳しくは以下を参照してください。

https://getcomposer.org/doc/05-repositories.md#types

typepathにしてインストールするとインストールしたプロジェクト内のパッケージの格納されているvendorディレクトリにローカルパッケージディレクトリのシンボリックリンクが生成されます。

パッケージのコーディングを行う際はプロジェクト内のvendor以下のパッケージを変更すれば、プロジェクトと同階層に作成したパッケージディレクトリのソースに反映されるので、IDEを使用している時はコードジャンプ等ができて便利です。

クラスやメソッドの作成

ちなみに自分はパッケージ名でメインとなるクラスを作成しています。

今回のパッケージだと以下の様な命名や構成になります。

├── LICENSE
├── README.md
├── composer.json
├── phpunit.xml
├── resources
│   ├── assets
│   │   ├── css
│   │   │   ├── jexcel.css
│   │   │   └── jsuites.css
│   │   └── js
│   │       ├── jexcel.js
│   │       └── jsuites.js
│   └── views
│       └── index.php
├── src
│   ├── DataBases
│   │   ├── BaseDriver.php
│   │   ├── Connect.php
│   │   └── Drivers
│   │       ├── Mysql.php
│   │       └── Pgsql.php
│   ├── Distancexport.php
│   ├── Provider.php
│   └── Renders
│       └── AssetResponse.php
└── tests
    └── DistancexportTest.php

Distancexport.phpがパッケージの起点となるクラスファイルです。

またresourcesディレクトリは今回のパッケージにはviewを描画する必要があったのでディレクトリを作成しています。

クラスを作成したらパッケージのcomposer.jsonにautoloadの設定を追記します。

{
    "name": "some/mypackage",
    "license": "MIT",
    "require": {},
    "autoload": {
        "psr-4": {
            "Some\\Mypackage\\": "src/",
            "Some\\Tests\\": "tests/"
        }
    }
}

これでsrc配下がSome\Mypackageのnamespaceでオートロードされます。

testsに関してはディレクトリ配下にテストコードが格納される前提なので、Some\Testsのnamespaceでオートロードされます。

またクラスを作成する際に命名するnamespaceですがオートロードで設定したnamespaceでないとファイルが読み込めません。

パッケージに実装するnamespaceを記述するファイルにはcomposer.jsonに記述した通りのnamespaceをつける必要があります。
(※上記例でいうとSome\Mypackage\ClassName

また外部のパッケージが必要な場合はrequireに記述すればプロジェクト側でこのパッケージをcomposer requireもしくはinstallした際にインストールしてくれます。

README.mdを作る

READMEを作成します。

自分は主に以下の様な項目を記載する様にしています。

  • パッケージ名
  • バージョンやライセンス等を表示するバッチ
    視覚的にわかりやすいし何よりカッコイイ:smiley:
  • 概要
    パッケージの概要を記述します
  • パッケージにUIがあればgifアニメや画像
    UIがあるものなら画像などがあった方がわかりやすいと思います。また、作成した画像でもいいかもしれません。
  • 目次
    各見出しのリンク付きの目次を配置します。
  • 特徴 パッケージの特徴です。
  • 要件 対応するバージョンなどを記載する箇所になります。
  • インストール方法 パッケージのインストール方法です。 コピペできる様にコマンドで記述します。
  • 使い方
  • ライセンス

今回公開したパッケージは以下の様な項目を記載しています。
https://github.com/temori1919/distancexport/blob/master/README.md

バッチに関しては以下のサービスを使用しました。

shields

packegistに公開してあるパッケージなら簡単にバッチを作成することができます。

もちろん公開していなくても作成できるバッチもあります。

スクリーンショット 2020-11-20 9.27.09.png

上記はpackagistのversionになりますが、公開されているベンダー名とパッケージ名を入力するとmdやhtmlでURLを生成してくれます。

バージョンに関してはpackagistに公開後でないと生成できないので、パッケージを公開し、後述するバージョンをタグ付けしたのちに作成することになると思います。

また、バッチのスタイルや文言を変更することが可能です。

GitHubにリポジトリを作成してpushする

リポジトリにソースをpushしましょう。
pushした後にLICENSEファイルを作成します。

Create new fileを押下し新しいファイルをGitHub上で作成します。
ファイル名にLICENSEと入力するとライセンスファイルのテンプレートが選べる様になります。
スクリーンショット 2020-11-20 9.37.45.png

後は用途にあったライセンスを選択してファイルを生成します。

タグでバージョンを指定する

tags->Create a new releaseで以下の様なタグ作成画面に遷移すると思うので、タグを生成していきます。

スクリーンショット 2020-11-30 8.54.27.png

Tag versionを指定します。

自分はv1.0.0からスタートする様にしています。
vがあってもなくても問題ありません。

composerはv付きでもバージョンとして認識します。

Tag versionの横にTargetブランチが選択されていますが、masterブランチの現時点での最新コミットが今回のタグ付けしたバージョンになります。

タイトル、詳細内容等あれば記述しますがなくても問題はありません。

packagistに公開する

最後にpackagistにGitHubのリポジトリを公開します。

アカウントがなければ作成してください。
GitHubアカウントでもログインすることが可能です。

スクリーンショット 2020-11-30 9.46.12.png

ヘッダーにあるSubmitボタンからパッケージを送信するページを表示します。

リポジトリのURLをRepository URLに記載してCheckボタンを押してください。

これでGitHubのリポジトリとpackagistが紐付来ました。
GitHubで追加したタグ情報もpackagistのパッケージバージョンとしてリンクする様になります。

リポジトリに追加してあるREADME.mdもpackagistのパッケージTOPページに反映されます。

GitHubのWebhookにpackagistのapiトークンを登録する

GitHubのWebhookにpackagistのapiのURLを登録します。

Webhookを活用することでGitHubのリポジトリを更新した際に自動でpackagist側に変更を反映させることができる様になります。

スクリーンショット 2020-12-03 9.39.12.png

Show API Tokenを押してトークンを表示してください。
その下のthe docsのリンクを押すとGitHubのWebhookの登録方法が表示されます。

上記のドキュメントに記載されているURLを表示したトークンを使ってGitHubにWebHookを登録します。

スクリーンショット 2020-12-03 9.46.47.png

ドキュメントに記載されている通りに入力してください。

これで公開に伴う全ての作業が完了しました。

最後に

手順を追ってみると、パッケージの公開はそこまでハードルの高いものではないと思います。

次回はLaravelのパッケージを公開する手順に続きます。

Composerパッケージを作ろう その② - Laravelパッケージ編

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

PHPの文字列連結の中で最速を考えてみる

はじめに

こんにちは、普段は業務で求人系サービスの開発を行なっている@taku-0728です。
今回はいくつかあるPHPの文字列連結のやり方について、一番早いやり方を考えてみます。
あくまで一例なので、他に「こんなやり方もあるよ!」等あればコメントにてご指摘いただければ幸いです。
よろしくお願いいたします。

背景

業務中にPHPで文字列連結をする場面があったのですがその時にふと
「普段は何も考えず'私の名前は' .$name .'です。'とか書いてるけど、
"私の名前は $name です。"とか書いても結果は同じだしどっちが早いんだろう」
と思ったので検証してみます。

準備

今回は下記3パターンの文字列連結の書き方を準備します。

パターン1(シングルクォート)

パターン1はシングルクォートと連結演算子(.)で連結したパターンです。
サンプルコードはこちら

pattern1.php
<?php
// パターン1
$dogName = 'ポチ';
$startTime1 = microtime(true);

for($j = 0; $j < 10000; $j++){
  $tmp = '私が飼っている犬の名前は' .$dogName .'です。';
}

$endTime1 = microtime(true);
$ret1 = $endTime1 - $startTime1;
?>

パターン2(変数展開)

パターン2は文字列の中で変数展開したパターンです。
サンプルコードはこちら

pattern2.php
<?php
// パターン2
$dogName = 'ポチ';
$startTime2 = microtime(true);

for($j = 0; $j < 10000; $j++){
  $tmp = "私が飼っている犬の名前は $dogName です";
}

$endTime2 = microtime(true);
$ret2 = $endTime2 - $startTime2;
?>

パターン3(ダブルクォート)

パターン3はダブルクォートと連結演算子(.)で連結したパターンです。
サンプルコードはこちら

pattern3.php
<?php
// パターン3
$startTime3 = microtime(true);

for($j = 0; $j < 10000; $j++){
  $tmp = "私が飼っている犬の名前は" .$dogName ."です。";
}

$endTime3 = microtime(true);
$ret3 = $endTime3 - $startTime3;
?>

検証

上記3パターンのコードを実行し、速度を検証してみます。
複数回実行した時に必ずしも同じ速度になるとは限らないので、今回は10回実行した結果の平均値で検証を行います。
それを踏まえた最終的なコードは下記です。
雑に考えただけなので、より簡潔な書き方があればコメントにてご指摘いただければ幸いです。

test.php
<?php
$dogName = 'ポチ';
$sum1 = 0;
$sum2 = 0;
$sum3 = 0;

for($i = 0; $i < 10; $i++){
  // パターン1
  $startTime1 = microtime(true);

  for($j = 0; $j < 10000; $j++){
    $tmp = '私が飼っている犬の名前は' .$dogName .'です。';
  }

  $endTime1 = microtime(true);
  $ret1 = $endTime1 - $startTime1;
  $sum1 += $ret1;

  // パターン2
  $startTime2 = microtime(true);

  for($j = 0; $j < 10000; $j++){
    $tmp = "私が飼っている犬の名前は $dogName です";
  }

  $endTime2 = microtime(true);
  $ret2 = $endTime2 - $startTime2;
  $sum2 += $ret2;

  // パターン3
  $startTime3 = microtime(true);

  for($j = 0; $j < 10000; $j++){
    $tmp = "私が飼っている犬の名前は" .$dogName ."です。";
  }

  $endTime3 = microtime(true);
  $ret3 = $endTime3 - $startTime3;
  $sum3 += $ret3;
}

$avg1 = $sum1 / 10;
$avg2 = $sum2 / 10;
$avg3 = $sum3 / 10;

echo("パターン1:平均時間:" .$avg1 ."秒\n");
echo("パターン2:平均時間:" .$avg2 ."秒\n");
echo("パターン3:平均時間:" .$avg3 ."秒\n");
?>

結果

10回実行した平均値は下記のようになりました。

パターン1:平均時間:0.00032708644866943秒
パターン2:平均時間:0.00034801959991455秒
パターン3:平均時間:0.00032320022583008秒

パターン2の変数展開が一番遅いのは予想通りでしたが、パターン1のシングルクォートよりもパターン3のダブルクォートの方が(わずかではありますが)速かったのは意外でした。
環境や実際のコードにもよるのでこの結果が全てのパターンに当てはまるとは限りませんが、参考にしてみてください。

まとめ

今回はPHPの文字列連結はどのやり方が一番早いのか、ふと疑問に思ったので検証してみました。
結果の方にも記載しましたが、あくまで一例ですので、参考程度にお考えください。
雑に書いたコードですので、もし誤りや改善点があればお手数ですがコメントにてご指摘いただけると幸いでございます。
ありがとうございました。

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

【WordPress】標準の投稿アーカイブのパーマリンクを任意のURLにする方法いろいろ【備忘録】

毎回忘れるので、備忘録として書いとこうと…

やりたいこと

  • WordPress標準の投稿(post)のアーカイブページを作って、https://www.example.com/スラッグ/みたいな任意のパーマリンクにしたい。
  • 標準の投稿のシングルページのパーマリンクを、https://www.example.com/スラッグ/ページスラッグorIDみたいな感じにしたい。

早い話が
「カテゴリとかもないただのお知らせ的なの実装するだけでわざわざカスタム投稿タイプ作るのめんどいし抵抗がある、かといって標準の投稿だとアーカイブがカテゴリーベースのhttps://www.example.com/category/hoge/みたいな感じでシングルページがhttps://www.example.com/123/みたいな感じになってなんか気持ち悪い」
という人向け。

やり方

方法① カスタム投稿タイプをつくる。

上に書いた早い話の鉤括弧内を見て『いやカスタム投稿でよくね?』と思う方はカスタム投稿タイプ作るほうが圧倒的に早いし、パーマリンク的にはすごい素直な感じの挙動します。標準の投稿が完全に無用の長物と化すけど。
やり方はregister_post_typeするときに

'rewrite' => array( 'slug' => '任意のスラッグ', 'with_front' => true),
'has_archive' => true

とするだけ。ググればたくさん情報出てきますのでググってください。標準の投稿? 隠せばいいのだ。

方法② アーカイブページ用の固定ページを作る

標準の投稿を活かすならこっちの方法。解説は後ほど。

方法③ 'has_archive' => trueにしてうまいことする →できない

この「うまいことする」をやろうとして調べて結局キレイな形でできないから②のやり方に移行する、というのを毎回やってます。
アーカイブページなのに固定ページってなんか気持ち悪いし、無駄な固定ページがあるのも気になるし、ていうかアーカイブページがarchive.phpじゃなくてpage-{slug}.phpで動いてるのがなんか気持ち悪いんでこのやり方でやりたいんですが、アーカイブページのパーマリンクをどう頑張っても取得できません。

カテゴリでも日付でもない通常のアーカイブページを有効にするのは以下で可能。ググれば出てくる。

function.php
// 通常アーカイブを有効にする
function post_has_archive( $args, $post_type ) {
    if ( 'post' == $post_type ) {
        $args['rewrite'] = true
        $args['has_archive'] = '任意のスラッグ';
    }
    return $args;
}
add_filter( 'register_post_type_args', 'post_has_archive', 10, 2 );

で、例えばスラッグをnewsとかにした場合、https://www.example.com/news/とURLを叩くとarchive.phpのテンプレで投稿一覧が表示される。
のだがこのhttps://www.example.com/news/WordPressの関数ではどう頑張っても引っ張ってこれない。
get_post_type_archive_link('post')で引っ張ってこようとしても、引数が'post'だと無常にもfalseが返ってくる。
URL直接叩かせるなり、アーカイブのリンク返す関数やショートコードを適当に自作するなり、post_type_archive_linkにフィルターフックしてうまいことするなりでなんとかしようと思えばできるけど、
アーカイブをarchive.phpで表示するという『当たり前にできそうなこと』をしたいから頑張ってるのにそんな力技で解決するのはなんか気持ち悪い。

というのと、WordPressのシステム上ではアーカイブページが無いものとして扱われるので(厳密には違うが)、例えばサイトマップ生成プラグインとか使ってるとアーカイブページが含まれなかったり、
それを解決しようとするとリライトルールをゴリゴリ書くハメになったりと、
とにかく不具合や複雑化の温床になりやすい。

ので、最終的に固定ページが1ページ増えるだけで特に他には影響が出ない【方法②】に落ち着くわけである。一応WordPress推奨の方法だしな…
今更説明するまでもないと思うけど、一応手順は以下の通り。

方法②-手順1. アーカイブ用の固定ページを作る

タイトルは任意のタイトル、本文は空でよい。このページのパーマリンクがアーカイブページのパーマリンクになる。
当然ながら、テンプレートとなるPHPファイルは用意しておくこと。

手順2. 表示設定・パーマリンク設定を変更

2020-12-08_03h38_09.png
↑表示設定の【投稿ページ】で今しがた作った固定ページを選択する。
2020-12-08_03h42_36.png
↑パーマリンク設定。【カスタム構造】のラジオボタンをチェックして、/任意のスラッグ/(任意のパーマリンク構造)とする。画像では%post_id%にしているが%post_name%でも構わない。

手順3. アーカイブのパーマリンクの取得方法

example.php
echo get_permalink( get_page_by_path( '固定ページのスラッグ' )->ID );

これで引っ張ることになる。うーん、やっぱりなんか…

ほんとお知らせ機能とか標準の投稿タイプそのまま使えたらそれで良いんだけど、パーマリンクが何か気持ち悪い感じになるのが如何ともし難い。
管理画面の見え方からカスタマイズするようなレベルならカスタム投稿タイプ使うことに抵抗はないんだけど、固定ページいくつか+αくらいのサイトを作るときはこの辺の仕様で毎回もにゃる。自分だけかもしれないけど。

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

30代ヨワヨワエンジニアがServerlessFramework(lambda)を使ってphpでslackにapiを使用して村上春樹風に語りながら猫画像を定期的に投稿するプログラムを書いてみた

早速ですが

僕の上司は猫です

なので、気まぐれで寂しがり屋な上司のためにも猫の画像を取得してslackに定期的に画像を投稿出来るように挑戦してみました。

今回の制作背景

上司のためでもありますが、入社して6ヶ月たちapiを使って連携する事が(cronも多い)ため実装する事でapi連携がわかるのではないかと思ったのと、lambdaの話をよく聞く事が多かったのでServerlessFrameworkを使用してslackに猫画像を定期的に投稿するプログラムを実装してみました

目次

1.開発環境
2.使用したAPI
3.コードとディレクトリ構造
4.苦労した事
5.良かった事
6.改善点
7.その他参考にした記事
8.おわりに

1. 開発環境

ServerlessFrameWork
Framework Core: 2.12.0
Plugin: 4.1.2
SDK: 2.3.2
Components: 3.4.0

php:7.3
bref:0.5.33

ServerlessFrameworkとは
公式ドキュメント

サーバーレスフレームワークを使用することで、オーバーヘッドとコストを大幅に削減して、サーバーレスアプリケーションを開発、展開、トラブルシューティング、および保護する

負荷やらお金を大幅削減できるらしいですね

lambdaはサーバーを用意する必要がなく、パッチ当てなどの保守作業を行わなくても良いため、利用者はインフラ管理の大部分をクラウドに任せて、開発リソースをつぎ込むことができるのがメリットのようです。
ServerlessFrameworkの中核的な存在ですね。

サーバレスアーキテクチャに関する説明が下記となります。
https://mmmcorp.co.jp/column/serverless/

2. 使用したAPI

TheCatApi
Slack

Api導入にあたり参考にさせて頂いたQiita記事
Catapi関連(参考にさせて頂いた部分が多かったです):https://qiita.com/nkojima/items/ac28780c5e61c6a9f999v
SlackBot関連:https://qiita.com/gkzz/items/23a7d03aeadcec341700

3. コードとディレクトリ構造

ディレクトリ構造

.
├── composer.json
├── composer.lock
├── index.php
├── serverless.yml
└── vendor
    ├── async-aws
    ├── autoload.php
    ├── bin
    ├── bref
    ├── composer
    ├── hollodotme
    ├── mnapoli
    ├── nyholm
    ├── php-di
    ├── php-http
    ├── psr
    ├── riverline
    └── symfony
serverless.yml
service: phpapp

provider:
    name: aws
    region: ap-northeast-1
    runtime: provided

plugins:
    - ./vendor/bref/bref

functions:
    function:
        handler: index.php
        description: ''
        layers:
            - ${bref:layer.php-73}
        events:
          - sqs:
              arn:
                Fn::GetAtt:
                  - PhpQueue
                  - Arn
          - schedule: cron(0/50 * * * ? *)  # 毎日50分おき

resources:
  Resources:
    PhpQueue:
      Type: AWS::SQS::Queue
      Properties:
        QueueName: phpqueue
index.php
<?php
declare(strict_types=1);
require __DIR__.'/vendor/autoload.php';

lambda(function ($response) {
$cat_url = 'https://api.thecatapi.com/v1/images/hogehoge'; // cat_apiのリクエストするURLとパラメータ

// curlセッションを初期化する
$curl = curl_init($cat_url);

// リクエストのオプションをセットしていく
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'GET'); // メソッド指定
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); // 証明書の検証を行わない
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); // レスポンスを文字列で受け取る

ここまでGET
-------------------------------------------------------------
ここからPOST

// レスポンスを変数に入れる
$response = curl_exec($curl);
// JSON文字列をデコードした結果を変数にいれる
$result = json_decode($response, true);
// urlのバリューを取り出して変数にいれる
$image = $result[0]['url'];

// slack_apiのURLとパラメーター
$url = 'https://hooks.slack.com/services/hogehoge';

// 配列にキーとバリューをいれる
$message = array(
  "username"   => "ユーザー名",
  "icon_emoji" => ":slack:",
  "text" => "やれやれ\n".$image,
);

// メッセージをjson化
$message_json = json_encode($message);

// payloadの値としてURLエンコード
$message_post = "payload=".urlencode($message_json);

// curlセッションを初期化する
$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, $url); // curlオプションを設定する
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // curl_exec()を実行時、返り値を文字列で返す。
curl_setopt($ch, CURLOPT_POST, true); // POST
curl_setopt($ch, CURLOPT_POSTFIELDS, $message_post); // CurlPostする際に必要
curl_exec($ch); // curlを実行する
curl_close($ch); // curlセッションを終了する
});

lambdaでGETしたら....

スクリーンショット 2020-11-24 1.23.25.png

取れた!後はURLをJSONからデコードしてURLのバリューを取り出して配列に入れてエンコードして....

結果

スクリーンショット 2020-12-06 18.52.15.png

出来た!後は50分おきに投稿されるだけ

スクリーンショット 2020-12-06 17.51.02.png

ドーン!!!

4. 苦労した事

  • lambdaのruntimeにphpがなかったので、customruntimeのbrefを使ったが設定に若干、てこずった。参考にしたbrefの設定リンク
  • jsonを一度、デコードしてから配列に変換し、その中の値を取り出して新たな配列に値を入れてエンコードする一連の作業に苦しめられた。
  • 作った変数の中にどの値が入っているか毎回、dumpして今の形式がどうなっているか細かなデバッグをしながら作業した。

5. 良かった事

  • curlでGETとPOSTする基本的な動作を行う事ができた
  • データをどう取ってきて、どう処理するかの一連の流れをより理解する必要がある事がわかった
  • apiの一連の流れを自分で考えながら(かなり助けていただきました)実装できた事。
  • GETとPOSTの関数は別にするのが通常だが、ワンライナーで試しにやってみて試行錯誤しながら出来た事。
  • serverless deployコマンドだけでlambdaにdeployが出来る事を知れたのは良かった。

6. 改善点

  • 当初、laravelとphpで実装しようとしていたが、phpのみの実装に変えた(そもそもlaravelを使うならlambdaを使うメリット、デメリットやコストパフォーマンスを理解してから実装する必要がありそう)
  • 今回は画像形式が文字列でslackに投稿してもslack上に画像を出す事が出来たが、本来あらゆる形式を扱えるようになる必要がある。
  • やれやれしか呟けなかった。色んな種類の春樹風を呟きたかった。

7. その他参考にした記事や簡単な用語メモ

Curl
インターネットプロトコルを使用してURLとして指定されたリソースのインターネット転送を行うツール(色んな言語で使える)
Curlでapi
エンコードとデコード
その他いろいろ

8. おわりに

個人的には勉強する部分が多かったので非常にためになりました!
プログラマ歴の浅い人でも知らない技術に果敢に挑戦出来るんだぞと勇気づける事が出来れば幸いです。
一緒に住んでいるシェアハウスの住民には色々と助けてもらいました。ありがとうございました。
業務ではまだまだ戦力としては雀の涙ですが、フロントとバックエンドも全て出来る職場で自由な環境で働けているのでありがたいです。つよつよ目指して引き続き頑張ります。

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

30代ヨワヨワエンジニアがServerlessFramework(lamda)を使ってphpでslackにapiを使用して村上春樹風に語りながら猫画像を定期的に投稿するプログラムを書いてみた

早速ですが

僕の上司は猫です

なので、気まぐれで寂しがり屋な上司のためにも猫の画像を取得してslackに定期的に画像を投稿出来るように挑戦してみました。

今回の制作背景

上司のためでもありますが、入社して6ヶ月たちapiを使って連携する事が(cronも多い)ため実装する事でapi連携がわかるのではないかと思ったのと、lambdaの話をよく聞く事が多かったのでServerlessFrameworkを使用してslackに猫画像を定期的に投稿するプログラムを実装してみました

目次

1.開発環境
2.使用したAPI
3.コードとディレクトリ構造
4.苦労した事
5.良かった事
6.改善点
7.その他参考にした記事
8.おわりに

1. 開発環境

ServerlessFrameWork
Framework Core: 2.12.0
Plugin: 4.1.2
SDK: 2.3.2
Components: 3.4.0

php:7.3
bref:0.5.33

ServerlessFrameworkとは
公式ドキュメント

サーバーレスフレームワークを使用することで、オーバーヘッドとコストを大幅に削減して、サーバーレスアプリケーションを開発、展開、トラブルシューティング、および保護する

負荷やらお金を大幅削減できるらしいですね

lamdaはサーバーを持たないため、パッチ当てなどの保守作業を行う必要がなく、利用者はインフラ管理の大部分をクラウドに任せて、開発リソースをつぎ込むことができるのがメリットのようです。
ServerlessFrameworkの中核的な存在ですね。

サーバレスアーキテクチャに関する説明が下記となります。
https://mmmcorp.co.jp/column/serverless/

2. 使用したAPI

TheCatApi
Slack

Api導入にあたり参考にさせて頂いたQiita記事
Catapi関連(参考にさせて頂いた部分が多かったです):https://qiita.com/nkojima/items/ac28780c5e61c6a9f999v
SlackBot関連:https://qiita.com/gkzz/items/23a7d03aeadcec341700

3. コードとディレクトリ構造

ディレクトリ構造

.
├── composer.json
├── composer.lock
├── index.php
├── serverless.yml
└── vendor
    ├── async-aws
    ├── autoload.php
    ├── bin
    ├── bref
    ├── composer
    ├── hollodotme
    ├── mnapoli
    ├── nyholm
    ├── php-di
    ├── php-http
    ├── psr
    ├── riverline
    └── symfony
serverless.yml
service: phpapp

provider:
    name: aws
    region: ap-northeast-1
    runtime: provided

plugins:
    - ./vendor/bref/bref

functions:
    function:
        handler: index.php
        description: ''
        layers:
            - ${bref:layer.php-73}
        events:
          - sqs:
              arn:
                Fn::GetAtt:
                  - PhpQueue
                  - Arn
          - schedule: cron(0/50 * * * ? *)  # 毎日50分おき

resources:
  Resources:
    PhpQueue:
      Type: AWS::SQS::Queue
      Properties:
        QueueName: phpqueue
index.php
<?php
declare(strict_types=1);
require __DIR__.'/vendor/autoload.php';

lambda(function ($response) {
$cat_url = 'https://api.thecatapi.com/v1/images/hogehoge'; // cat_apiのリクエストするURLとパラメータ

// curlセッションを初期化する
$curl = curl_init($cat_url);

// リクエストのオプションをセットしていく
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'GET'); // メソッド指定
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); // 証明書の検証を行わない
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); // レスポンスを文字列で受け取る

ここまでGET
-------------------------------------------------------------
ここからPOST

// レスポンスを変数に入れる
$response = curl_exec($curl);
// JSON文字列をデコードした結果を変数にいれる
$result = json_decode($response, true);
// urlのバリューを取り出して変数にいれる
$image = $result[0]['url'];

// slack_apiのURLとパラメーター
$url = 'https://hooks.slack.com/services/hogehoge';

// 配列にキーとバリューをいれる
$message = array(
  "username"   => "ユーザー名",
  "icon_emoji" => ":slack:",
  "text" => "やれやれ\n".$image,
);

// メッセージをjson化
$message_json = json_encode($message);

// payloadの値としてURLエンコード
$message_post = "payload=".urlencode($message_json);

// curlセッションを初期化する
$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, $url); // curlオプションを設定する
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // curl_exec()を実行時、返り値を文字列で返す。
curl_setopt($ch, CURLOPT_POST, true); // POST
curl_setopt($ch, CURLOPT_POSTFIELDS, $message_post); // CurlPostする際に必要
curl_exec($ch); // curlを実行する
curl_close($ch); // curlセッションを終了する
});

lamudaでGETしたら....

スクリーンショット 2020-11-24 1.23.25.png

取れた!後はURLをJSONからデコードしてURLのバリューを取り出して配列に入れてエンコードして....

結果

スクリーンショット 2020-12-06 18.52.15.png

出来た!後は50分おきに投稿されるだけ

スクリーンショット 2020-12-06 17.51.02.png

ドーン!!!

4. 苦労した事

  • lambdaのruntimeにphpがなかったので、customruntimeのbrefを使ったが設定に若干、てこずった。参考にしたbrefの設定リンク
  • jsonを一度、デコードしてから配列に変換し、その中の値を取り出して新たな配列に値を入れてエンコードする一連の作業に苦しめられた。
  • 作った変数の中にどの値が入っているか毎回、dumpして今の形式がどうなっているか細かなデバッグをしながら作業した。

5. 良かった事

  • curlでGETとPOSTする基本的な動作を行う事ができた
  • データをどう取ってきて、どう処理するかの一連の流れをより理解する必要がある事がわかった
  • apiの一連の流れを自分で考えながら(かなり助けていただきました)実装できた事。
  • GETとPOSTの関数は別にするのが通常だが、ワンライナーで試しにやってみて試行錯誤しながら出来た事。
  • serverless deployコマンドだけでlamdaにdeployが出来る事を知れたのは良かった。

6. 改善点

  • 当初、laravelとphpで実装しようとしていたが、phpのみの実装に変えた(そもそもlaravelを使うならlambdaを使うメリット、デメリットやコストパフォーマンスを理解してから実装する必要がありそう)
  • 今回は画像形式が文字列でslackに投稿してもslack上に画像を出す事が出来たが、本来あらゆる形式を扱えるようになる必要がある。
  • やれやれしか呟けなかった。色んな種類の春樹風を呟きたかった。

7. その他参考にした記事や簡単な用語メモ

Curl
インターネットプロトコルを使用してURLとして指定されたリソースのインターネット転送を行うツール(色んな言語で使える)
Curlでapi
エンコードとデコード
その他いろいろ

8. おわりに

個人的には勉強する部分が多かったので非常にためになりました!
プログラマ歴の浅い人でも知らない技術に果敢に挑戦出来るんだぞと勇気づける事が出来れば幸いです。
一緒に住んでいるシェアハウスの住民には色々と助けてもらいました。ありがとうございました。
業務ではまだまだ戦力としては雀の涙ですが、フロントとバックエンドも全て出来る職場で自由な環境で働けているのでありがたいです。つよつよ目指して引き続き頑張ります。

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

AWSのcloud9でPHPのバージョン古い件

スクリーンショット 2020-12-06 11.11.59.png
WordPressのカレンダープラグインを使ってウェブサイトのカスタマイズの案件を受注したので、久々にAWSで開発環境を作ろうとしました。

でも、ネットで書かれているWordPressの開発環境の手順で行っても、VFS connecion does not existとエラーになってしまいプレビューができない状態です。

スクリーンショット 2020-12-06 11.11.59.png

そのため、色々とネットで調べたのですが、EC2インスタンスの再起動しか方法が見つからず、途方にくれていました。

こちらに記録しましたが、AWSのWordPressのサンプルのドキュメントを参考にして、設定作業を行っていました。

https://teratail.com/questions/308472
テラテイルの回答を見ていて、私のPHPのバージョンが古いことに気がつきました。
そのため、こちらの記事を基に、PHPの7.3をインストールしました。
https://qiita.com/gomiryo/items/28c07d5ee9afd1c325b0

私がWordPressの開発環境作りに参考にした記事は、こちらです。
https://upd.world/wordpress-cloud9/

https://skillhub.jp/blogs/197

https://qiita.com/acecrc/items/a72992a17968d3d9bc00

https://laboratory.kazuuu.net/what-to-do-when-oops-vfs-connection-does-not-exist-is-displayed/

PHPのバージョンを上げたらWordPressのインストールは、できました。
でも、管理画面へのログインができない状態です。
そのため、もう一度環境を作り直そうと思っています。

クライアントの作業のこともあり、cloud9で開発環境作りばかりできないので、断続的に調査復旧を行っていきたいと思っています。

cloud9のIDEの中でタグを使ってアプリケーションの表示ができても、ブラウザにタグを追加して表示ができない場合もあります。
AWSのインターネット共有のドキュメントを読み、設定の確認を行っていきたいと思っています。

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