伊藤清徳の垂直落下式ムーンサルトプレス

PerlとかPHPとかMySQLとか...がんばっても8割だ。

Category: PHP (page 1 of 6)

BigSur + MAMP Pro + PostgreSQLを構築

どうもお久しぶりです。相変わらずへんてこなことをやっている伊藤です。

今日は、mac os BigSurとMAMPとPostgreSQLでローカル開発環境を作ったお話です。
アーキテクチャはM1(apple silicon)です。

あんまりMAMP+PostgreSQLの情報は日本語サイトではないので、久しぶりにブログ書きます。

M1における MAMP ProとPostgreSQLについて

MAMP ProもPotgreSQLもM1 macにおいて動作します。が、2021年3月時点では、アーキテクチャはintelとして動きますので、ARMネイティブではないのでご注意を。

PostgreSQLについて

MacでのPostgreSQLは、ググるとhomebrewつかいましょうねとなっているところが多いです。
が、ちょっと面倒なんで僕はEnterpriseDBを使っています。
ただ、EDBをインストールすると常時デーモンとして起動してしまうので、バッテリー食いですから必要なときのみ起動したいので、インストール後に

sudo launchctl unload /Library/LaunchDaemons/com.edb.launchd.postgresql-x.x.plist
sudo rm /Library/LaunchDaemons/com.edb.launchd.postgresql-x.x.plist 

で、デーモン自動起動を停止します。(x.xのところはインストールバージョンによってかわります)

手動起動および停止は

sudo -u postgres /Library/PostgreSQL/9.4/bin/pg_ctl -D /Library/PostgreSQL/x.x/data start
sudo -u postgres /Library/PostgreSQL/9.4/bin/pg_ctl -D /Library/PostgreSQL/x.x/data stop

で行います。(x.xのところはインストールバージョンによってかわります)

PHPからの接続について

BigSurでのMAMP PHPからPostgreSQLの接続では

  • BigSur OSデフォルトのPHPにはPostgreSQLのドライバが入っていないので、CLI実行で問題が出る
  • 最新版PostgreSQLでは認証にsha256が利用されており、暗号化の問題で接続できない

という2点が問題になります。対応する必要があります。CLI実行時は特にフレームワークのマイグレーションなどで問題が出ますね。

CLI実行時はMAMPのPHPを利用する

CLI実行時は、MAMPのPHPを利用するようにすることで対応可能です。MAMPのPHPはpostgresql用ドライバが問題なくはいっており、動きます。

/Applications/MAMP/bin/php/php7.3.24/bin/php -c "/Applications/MAMP/bin/php/php7.3.24/conf/php.ini" yii migrate/up

例としてはこんなかんじです。(PHPが7.3.24の場合)。念の為php.iniを指定して実行しています。なお上記のコマンドは、僕が使っているYii2フレームワークのマイグレーション実行コマンドです。(Laravelより高速開発可能ですよー。という宣伝)

接続認証方式をMD5にする

EDBのPostgreSQLでは、接続認証がsha256方式となっている。
PDO-PostgreSQLドライバーでは古いMD5認証であるため認証が通らない。
「authentication requires libpq version 10 or above」というエラーが出る。
ので、PostgreSQL側の設定を変える必要がある。

https://mebee.info/2021/01/26/post-28446/
▲ここを参考に書き換える。(参考サイトの方ありがとうございます)

EDBでは、postgresql.confおよびpg_hba.conf
/Library/PostgreSQL/x.x/data
内にあります。(x.xはバージョンに合わせてください)

が、しかし、このフォルダは、postgresユーザーのみアクセス可能になっているため、GUIでは書き換えに行けない。
ターミナルで、sudoを指定した上で、vimやらnanoやらで書き換えてください。

書き換えたらPostgreSQLを再起動したらOK!

PostgreSQLいいよ!

PostgreSQLは久しぶり。webだとMySQLがどうしても多くなっちゃうけど、PostgreSQLの優位性は高い。今回は工場の基幹システムをPHPでつくるというなかなかにピーキーな案件ですが、PostgreSQLの堅牢さと柔軟さがあれば、web屋でもこんな案件取るチャンスあるよ!

現場からは以上です。アディオス!!

さくらインターネットのMySQLが5.7になってORDER BYでこまったとき

古いWebアプリケーションフレームワークをつかい
MySQL5.6までのMySQLデータベースをつかったアプリケーションを
MySQL5.7のサーバーに移転するにあたり

ORDER BY clause is not in SELECT list, references column...

というようなエラーが出た場合、MySQL5.7より「ONLY_FULL_GROUP_BY」がオンになっていることが原因だ。

このあたりは、すでに多くの先駆者が設定について書いてくださっている。

など。大変ありがたい情報だ。

しかし、さくらインターネットでは権限がない

さくらインターネットなど共有サーバーでは、これらの設定については、
権限付与がなく設定ができない。

さてどうしたものか。。。

そのセッションのみで、(MySQL Workbenchをつかっている経験から)SQLMODEの変更ができるはず。
ということで

SET SESSION sql_mode = 'NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';

毎回接続時に実行できればOKなはず。と考察して、調べた結果問題なしでした。
各フレームワークや、自作のシステムの場合も、
mysqli_connect関数の直後や、new PDO()直後あたりで毎回実行するようにすればOKです。

WordPressでタイトルタグをテンプレートファイル側から指定

今日はかりゆしウェアを着ている伊藤です。どうもどうも
2018年日本は災害レベルで暑いですね。沖縄県はすずしいですよ。

さて最近連日WordPressの話題を書いています。
今日はWPのタイトルタグをテンプレート側から指定できるといいよね~ってはなしをします。

おともだちのKさんの記事を参考にして書いていきます。

前提条件

  • 包括的に全部を書き換えるのではなく、必要なタイミングでテンプレートごとに指定したい
  • テンプレートファイルからは、get_header();の直前に関数で指定したい

たぶんあるあるだと思います。SEO考えるとね。結構いじりたいところですからね。

document_title_partsフィルターを利用する

Kさんの記事によると、document_title_partsに紐づけした関数に、引数としてタイトル構造を持つ配列が渡されて、それを編集してreturnすればいいぜ。ワイルドだろー?って書いてあるます。なるほど。しかし僕のやろうとしていることだと、必要なタイミングで呼びたいという前提と、テンプレート側から指定したという条件が入るので、そのまんまは使えません。
そこで登場するのがクロージャーです。

クロージャーを使って実装

もうめんどいからソースを晒します。functions.phpにでも書いてください。

/**
 * タイトル変更
 * @param string $title
 * @param string $tagLine
 * @param string $siteTitle
 */
function setTitle( $title, $tagLine = null, $sitTitlte = null ){
    add_filter(
        'doment_title_parts',
        function( $return ) use ( $title, $tagLine, $sitTitlte ) {
            $return['title'] = $title;
            if(!is_null($tagLine)){
                $return['tagline'] = $tagLine;
            }
            if(!is_null($sitTitlte)){
                $return['site'] = $sitTitlte;
            }
            return $return;
        }
    );
}

こんかんじ。で、テンプレからは

setTitle( 'たいとるだよーん' );
get_header();

もう少しちゃんとするならクラスにして静的メッドにするのが筋でしょうねー。
クロージャーをつかっています。あんまりWPでは使われないかとおもいます。

意外に使うTipsになるのでは?
現場からは以上です。

WordPressにORMを組み込む

どうもどうも。2年近く連絡をしていなかったら育ての親である叔父が心配してきた伊藤です。

さて、WordPressにはデータベースを操作する機能というのは一応揃っています。
wpdbとかそのへんですね。ただ、基本的にはSQLを書くか、あるいは、WPのもともとのテーブル構造に対するクエリを前提としていることが多く、遠回りだったりとかしますわね。できればActiveRecordやORMがはいっているといいなーと思うわけです。

特に、従前のシステムがあって、WPでリニューアルをかけたい。というときは、従前のシステムのテーブルはそのままにリニューアルをしたいということは、多々あるわけです。

まさにそんな事を考えていたので、いっちょWordPressにORMを入れてみた。というはなしをします。

PHPのActiveRecord ORMライブラリ

PHPのActiveRecordやORMの単体ライブラリというのが、実はそもそもあまりないです。最近のPHPのフルスタックWebアプリケーションの場合、Doctrineがベースに利用されていたりとかするわけで、車輪の再開発を嫌うのか、あまりないのです。

僕の場合は、PHP5.3系の頃は、php-activerecordをよく使っていましたが、ちょっと古くなったのと、リレーションのリレーションが追えないなど、ちょっと足らないところもあります。

今回探していていい感じだなーとおもったのが、RedBeanPHPです。MySQL/MariaDBのプラグインとPHPライブラリを提供しており、かなり高速で、わかりやすいコード体系のORMになっています。しかし、MySQLのプラグインということで、共有ホスティングなどでは可搬性がさがりますので、今回は見送りました。

今回採用することにしたのがSpotORMです。Doctrine DBALをベースに作られたORMラッパーです。Relationの定義などがしやすく、僕がよく使うフレームワークYiiのModelの構造に似ているのも、採用の理由です。

ではインストール

composerでインストールします。

{
    "require": {
        "vlucas/spot2": "~2.0"
    }
}

とかですかね。composerの使い方はぐぐれ。

WPの場合は、functions.phpにコードを書く感じになるので、テンプレートディレクトリで実行すればよいかと思います。で、functions.phpの冒頭くらいに

require __DIR__.'/vendor/autoload.php';

とでも書いておけば良し。インストールが簡単です。

つかってみましょう

まず接続設定をします。

$cfg = new \Spot\Config();
$cfg->addConnection( 'mysql', 'mysql://ユーザー:パスワード@ホスト名/DB名' );
$spot = new \Spot\Locator($cfg);

DSNで書くとこんなかんじ。連想配列での指定ももちろん可能です。
設定はWPの接続情報を使い回すとかでもいいでしょう。

モデルを用意する

ORMなのでモデルクラスを用意します。
僕の場合は、functions.phpの並びに「Entity」フォルダを用意して、その中にモデルクラスを配置しました。オートローダーをとかを作るほどでもないので functions.phpに

$files =  glob( __DIR__ .'/../Entity/*.php') ;
if(!is_array($files)){ $files = array(); }
foreach( $files as $one ){
    require_once $one;
}

を追記します。
Entityの中身を一気にロードしちゃっています。もちろん必要時に読むというやり方のが正しいとは思います。

Modelファイルの中身は

 ['type' => 'integer', 'primary' => true, 'autoincrement' => true],
        ];
    }
}

仮にこんなかんじです。fieldsになにかないと怒られることがある?っぽいので、primaryのIDだけ定義しておきました。

呼び出しに使ってみる

最後に実際にDBにクエリしてみましょう。
呼び出しをするfuncitonの中で

function hoge(){
    global $spot;
    $postMapper = $spot->mapper('Entity\Products');
    $row = $postMapper->where([
        'item_id' => '品番'
    ])->first();
    return $row;
}

こんな感じにかきます。上記例では商品DBに品番を問い合わせる。的な事をしています。

実際組み込んでいるがWPと融合しているわけではない

ここまで読むとPHPerならおわかりになるとおもいますが、WordPressのオブジェクトの他に、DBに対して接続を一つたてることになります。したがって、メモリ効率などは悪くなります。手軽さと、パフォーマンスどちらを優先するかなどで、選択することになるとおもいます。
しかし、DB操作をして、サイトデザインをWPに合わせるなどを考えると、有用ですし、SQLを書くよりはセキュリティ面でも安心してかけますね。

現場からは以上です。

超簡単なテンプレートエンジン(条件分岐なし)ならCodeigniterからパクれ

どうもどうも。今年はそこそこブログを書いています。体調が戻ってきました。よかったよかった。決して万全ではないのですが、1年近く体調がわるかったので、まぁそのいろいろアレです。

ところで、PHPerのみなさん。
たとえば、管理画面でメールテンプレートを管理するような場合、
そのテンプレートをどう処理するか、結構悩みますよね。

同じようなかんじで、WordPressなんかで、囲みタグ型のショートコードでループをさせたいときとかもかなり悩みます。

よくある例だと、

  1. 管理画面からテンプレート編集
  2. POSTされたデータをDBに保存しつつ、テンプレートエンジン用に、ファイルにも保存
  3. 出力時に保存されたファイルをテンプレートエンジンで処理して出力

というような手順でしょうか。
まぁ悪くはないですが、セキュリティに気をつけなければいけないとか、結構手間取りますし、ファイル出力だと手軽さがなくなります。

今回はWordPressのショートコードでどうしようかなーと思ったときの対処をメモ代わりに。

やりたいこと

前提条件です。

  • WordPressの囲みタグ型ショートコードをテンプレートとして扱いたい
  • テンプレートエンジンとして動かしたいが、PHPを書くのや、Smartyのようにファイル処理を前提としない
  • 条件分岐はとりあえずいらないがループはある

という前提条件で、具体的には

[get_product_categories category="カテゴリ名"]
<ul>
    {categories}
    <li>
        <a href="{category_url}">
           {category_name}
        </a>
    </li>
{/categories} 
</ul>
[/get_product_categories]

こんなショートコードを書いてもらってテンプレート処理をしたいわけです。
囲みタグ型のショートコードについてはみなさまが書いているので、そちらをご参考に

まぁ普通にやれば

このような条件を満たそうと思うと、難しいコードでもないので、自分で簡単なテンプレートエンジンを書いてもいいとは思うのですが、いまいち気乗りがしません。自分で書くとすれば、正規表現を書いて、ゴニョゴニョとするところではありますよね。

そこで!Codeigniterの簡易パーサークラスをぱくってきます

Codeigniterには条件分岐はないですがループに対応して、ファイル処理なしで実行できる簡易テンプレートンジンがついています。これが使えないかと思ってソースを見たところ、Codeigniter本体にはほとんど依存していないことがわかり、少しの書き換えで対応ができることがわかりました。

四の五の言わずにコードを晒せ

はい。

<?php
class CI_Parser {

    /**
     * Left delimiter character for pseudo vars
     *
     * @var string
     */
    public $l_delim = '{';

    /**
     * Right delimiter character for pseudo vars
     *
     * @var string
     */
    public $r_delim = '}';


    // --------------------------------------------------------------------

    /**
     * Class constructor
     *
     * @return void
     */
    public function __construct(){
    }

    // --------------------------------------------------------------------

    /**
     * Parse a template
     *
     * @param  string
     * @param  array
     * @param  bool
     * @return string
     */
    public function parse( $template, $data, $return = FALSE ){

        return $this->_parse($template, $data, $return);
    }

    // --------------------------------------------------------------------

    /**
     * Parse a template
     *
     * Parses pseudo-variables contained in the specified template,
     * replacing them with the data in the second param
     *
     * @param  string
     * @param  array
     * @param  bool
     * @return string
     */
    protected function _parse( $template, $data )
    {
        if ($template === '')
        {
            return FALSE;
        }

        $replace = array();
        foreach ($data as $key => $val)
        {
            $replace = array_merge(
                $replace,
                is_array($val)
                    ? $this->_parse_pair($key, $val, $template)
                    : $this->_parse_single($key, (string) $val, $template)
            );
        }

        unset($data);
        $template = strtr($template, $replace);

        return $template;
    }

    // --------------------------------------------------------------------

    /**
     * Set the left/right variable delimiters
     *
     * @param  string
     * @param  string
     * @return void
     */
    public function set_delimiters($l = '{', $r = '}')
    {
        $this->l_delim = $l;
        $this->r_delim = $r;
    }

    // --------------------------------------------------------------------

    /**
     * Parse a single key/value
     *
     * @param  string
     * @param  string
     * @param  string
     * @return string
     */
    protected function _parse_single($key, $val, $string)
    {
        return array($this->l_delim.$key.$this->r_delim => (string) $val);
    }

    // --------------------------------------------------------------------

    /**
     * Parse a tag pair
     *
     * Parses tag pairs: {some_tag} string... {/some_tag}
     *
     * @param  string
     * @param  array
     * @param  string
     * @return string
     */
    protected function _parse_pair($variable, $data, $string)
    {
        $replace = array();
        preg_match_all(
            '#'.preg_quote($this->l_delim.$variable.$this->r_delim).'(.+?)'.preg_quote($this->l_delim.'/'.$variable.$this->r_delim).'#s',
            $string,
            $matches,
            PREG_SET_ORDER
        );

        foreach ($matches as $match)
        {
            $str = '';
            foreach ($data as $row)
            {
                $temp = array();
                foreach ($row as $key => $val)
                {
                    if (is_array($val))
                    {
                        $pair = $this->_parse_pair($key, $val, $match[1]);
                        if ( ! empty($pair))
                        {
                            $temp = array_merge($temp, $pair);
                        }

                        continue;
                    }

                    $temp[$this->l_delim.$key.$this->r_delim] = $val;
                }

                $str .= strtr($match[1], $temp);
            }

            $replace[$match[0]] = $str;
        }

        return $replace;
    }

}

Codeigniterのコントローラーオブジェクトに依存しているところなどを削って純粋にclassとして動くようになっています。

じゃWPで動かしてみよう

functions.phpあたりで、上記ファイルをrequireして

add_shortcode( 'get_product_categories', 'get_product_categories');

functionget_product_categories( $atts, $content = null ){

    //このへんで適当に引数処理してね    
        
    $parser = new CI_Parser();
    $args = array( ここに適当に条件かいてね );
    $the_query = new WP_Query($args);
    if (!$the_query->have_posts()) {
        return '';
    }
    
    $data = array();
    $data['categories'] = array();//ショートコードのループタグと名前を合わせる
    while ( $the_query->have_posts() ) {
      $the_query->the_post();
      $set = array(
        'category_name' => get_the_title(),
        'category_url' => get_the_permalink(),
      );
      $data['categories'][] = $set;
    }
    wp_reset_postdata();
    
    
    $content = $parser->parse( $content, $data );//最後にパースする。
    return $content;
}

とでもしておけば、簡単ね。

多分改造でifくらいはいける

上記の通りCodeigniterのパーサーはシンプルにできてます。
たぶん改造すればifくらいは余裕でかけると思います。
が、他方、毎回実行時にパーサーが走るわけなので、決して軽くはありません。
使い所見極めていきましょう。

なお、Codeigniter3.xはMITライセンスなのでOKですが、2.xまでのコードを使う場合は独自ライセンスなので、扱いに気をつけてください。

現場からは以上です。

CentOS7+PleskOnyxな環境でのはまりごと

CentOS7 + PleskOnyxな環境をはじめて借りてハマったことをメモ

イニシャルではApacheモジュール版PHPがうごいていない

Pleskではすこし前のバージョンから

  • PHPの動作モード
  • PHPのバージョンを選択

できるようになっていますが、PleskOnyxでは、PHPの動作モードが

  • FAST CGI
  • FMP

のみになっており、あれ?Apacheモジュールは???となりました。
サーバーを借りたクララオンラインさんに、サポートをおねがいしたところ
http://faq.clara.jp/index.php?solution_id=1350
をご紹介くださいました。なーるほど。Pleskはもともといろーんなものがモジュール化していますが、
mod_php自体もモジュールなんですね。


CPANモジュールが共通ライブラリディレクトリにインストールできない

sshでログインした際に自動的に環境に

  • PERL5LIB=/ユーザーディレクトリ/perl5/lib/perl5:
  • PERL_MB_OPT=–install_base /ユーザーディレクトリ/perl5
  • PERL_LOCAL_LIB_ROOT=:/ユーザーディレクトリ/perl5
  • PERL_MM_OPT=INSTALL_BASE=/ユーザーディレクトリ/perl5

がセットされる。
これに気づかずに3時間なやんだ。とっとと
perl -V
してれば気づいたのに。。。

これら環境変数を削除した上で、CPANインストールすればよろし。


cgi-binディレクトリが有効になっていない

まぁPerl/CGIとかRuby/CGIとかやんなやって話ですが、昔の枯れた技術というのはとても有用でしてね…
で、イニシャル状態では、公開ディレクトリにcgi-binディレクトリがあって、
そっちが有効になっているんです。昔良くあった構成

/ユーザーディレクトリ/cgi-bin/
/ユーザーディレクトリ/httpdocs/

って言う構成がとれないわけです。
ので、ふるいサイトを移行するとちょっとめんどい。

/ユーザーディレクトリ/httpdocs/cgi-bin/

の中に格納すればいいわけですが。


1個1個はどうってことない話ですけど
勘がない環境だとハマりますね。って言うメモです。

NW.JSのPHP版 nightrain

node.jsでネイティブPCアプリを作ることのできるNW.js(旧Node-Webkit)はご存知の方は多いでしょう。
JavascriptというWEBの言語で、ちょっとしたツールを作ったり、デスクトップガジェットを作ったりできるのは、
WEB寄りのプログラマやエンジニアにとっては、嬉しいツールです。
 
で、そんな話をしていると、大体出てくる要求としては、PHPやPerlやRubyで作れないの?って言う要求ですね。
調べてないですが、多分Rubyはありそうです(文化面で)。PerlはPerl/Tkでがんばりましょうや。
PHPは?って探すと、NW.jsのPHP版とも言うべき、nightrainというのがあります。
 
それの話をば。。。


ディレクトリ構成


アプリケーションパッケージを展開すると、こんなディレクトリ構造になっています。
settings.iniでウインドウ状態の定義や、公開ディレクトリ(でいいのか?)の定義をします。
 
定義ファイルを見ると、Macの場合は、Mac標準で入っているPHPを利用するように定義してあるようです。
ということは、相対パスをつかってパッケージングすれば、バージョン違いのPHPを利用したり、
nightrain専用のphp.iniを持ったPHPを使えるということでしょうか。その辺は、未検証ですが。
 


公開ディレクトリ

公開ディレクトリは「www」内で、とりあえずCodeigniterを入れてみたところ、普通に動きました。
普通のWEBサイトのように動いてくれるようです。
__DIR__などのマジック定数もちゃんと処理してくれますし、composerもいけました。 한국 남성들은 성판매 여성을 찾아 웹사이트에 강남오피 검색하는데 강남오피는 여성들이 손님과 만나는 오피스텔입니다.
 
まぁPHPなんでマルチスレッドには対応してなかったりなど、悲しいところは
そのままなんですが、広範なライブラリを持つPHPでちょこーっとアプリを作れるのは、
やはり大きいですねー。
 


ところがですね

こんな便利なnightrainなんですが、開発をやめちゃったようで、
プロジェクトページが閉鎖になっています。
 
どうも公開のリポジトリは残っているようなんですが、ビルドがめんどくせぇ。ということで、
Mac版バイナリが見つかったので、
それを共有するために、この記事を書きました。
 
ダウンロードはこちらから
ライセンスは、MITライセンスです。
 
現場からは以上です。

WordPressに無理やりYii2を入れてみた。

PHPerの悩みどころっていうところで、
WordPressはWordPressのお作法があって、PHPの開発手法とは別だったりとか、
WordPressはMVCでなかったり、とか、スカッフォールディングあったらいいのにとか。
いろいろ悩むのである。
WordPressを拡張したり、プラグインをつくったりというのは、
WordPress使いが思うより、遥かに大変なのである。。。
 


フレームワーク使えたらなぁ

とはいえ、WordPressベースの案件というのはかなり数はある。
さてどうしたものか。
 
僕は普段Yii、Yii2というフレームワークをよく利用する。
http://www.yiiframework.com/
日本ではあまり使われないのだけれども、、
(CakePHPとか、Laravelがよく使われるのかな)
スカッフォールディングが非常に使いやすい、
既存のライブラリに頼らず、しっかりとチューニングしている、
なによりモデルの仕様が非常にわかりやすい。
などの理由でYiiを好んで使っている。
余談だが、日本における有名なYiiの使い手さんは田中ひさてるさんです。
 
閑話休題。
 
WordPressベースの案件にしても、何か簡単にデータベースの管理ができるといいなぁというのが、
PHPerの本音だったりするのでは…。
 
というわけで、どうにかしてみようと思った次第。
その話をしましょう!


Yii1.x系ならプラグインがある!

http://cornernote.github.io/yii-embed-wordpress/
YiiをWordPressのプラグインとしてインストールできるようにしてみようという試みのプラグイン。
うん。すごい。
 
使ってみた。そのまんまではスカッフォールディングなどは使えず。ちょこっと改造が必要。
それについては今度どこかで。
それでもYii1系は非常に見通しが良いフレームワークでPHP5.1.0以降をサポートしている。
この点においては、有用だと思う。
 
ただし、このプラグイン自体がメンテされていないのと、
Yii1.x系も公式には2015年末でサポートを切る予定(セキュリティ面での調整は当面続く)なので
その辺りを十分に認識した上で、利用していく必要がある。
 
やはりYii2でいきたいね。


Yii2でやる!

Yii2でやる!俺はやる!!と決めた。

どうしよう。Yii1のプラグインのようにやっても良いが…簡単ではない…
で、考えた、テーマディレクトリにインストールしたらどうか?
調べたら、codeigniterでやっている人がいた。

なるほど!single.phpなどindex.php以外のテーマファイルがなければ、自動的にindex.phpが、まず呼ばれる。
多くのフレームワークもindex.phpを起点としてルーティングしているので、
そのことを利用すれば、フレームワークをテーマディレクトリにインストールすることはできる!


テーマディレクトリにYii2をインストール

今回は、twentyfifteenテーマをコピーする形で「yii2」というテーマを作っていくことにしましょう。
子テーマで作るなどやり方はいくつかありますが、学習のためということで。
 
下準備として、WordPressをインストール後、
パーマリンク設定もGETパラメータを不要とする設定にしておいてください。
 
まず、wp-content/themes/yii2ディレクトリをつくります。
 
それでその中にYii2をインストールしますが、Yii2はcomposerでインストールします。
WordPressユーザーにはもしかするとcomposerは馴染みがないかもしれません。
composerはPHPのライブラリなどのパッケージをインストール管理をする仕組みです。
最近はアプリケーションのコアをcomposerで提供するなど利用が進んでいます。
Yii2など最近のフレームワークではインストールをcomposerでインストールすることを推奨することが多いので、
覚えておいて損はないでしょう。
 
というわけで、yii2ディレクトリを黒い画面で操作することになります。
 
まずはcomposer自体のインストールを。(Windowsの場合はまた違うの注意)

curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer

 
つぎにYii2をインストール

cd wp-content/themes/yii2
composer global require "fxp/composer-asset-plugin:~1.0.0"
composer create-project --prefer-dist yiisoft/yii2-app-basic .

 
つぎ、WordPressでは、テーマの構成として

  • index.php
  • style.css
  • functions.php

を最低限必要なファイルとなっています。
index.phpは、YiiによってインストールされているのでOKですから、
style.cssとfunctions.phpをtwentyfifteenフォルダからyii2フォルダにコピーします。
 
Yii2のインストール初期値では、
webという名前のディレクトリを公開ディレクトリして想定していますので、
構造を変更します。
wp-content/themes/yii2/web/
ディレクトリ内の各ファイルを
wp-content/themes/yii2/
ディレクトリへ移動し、
wp-content/themes/yii2/web/
を削除します。
 

これで「インストールの作業」は終了です。
ここからは「設定の作業」です。


style.css

WordPressでは、テーマ名などをstyle.cssの冒頭コメントから取得する仕組みになっていますので、
まずstyle.cssを変更しましょう。

/*
Theme Name: Yii2
Theme URI: http://agilmente.com/
Author: the Kiyonori Ito
Author URI: http://agilmente.com/
Description: Yii2 on WordPress Test!
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Tags: black, 
Text Domain: Yii2

This theme, like WordPress, is licensed under the GPL.
Use it to make something cool, have fun, and share what you've learned with others.
*/

冒頭をこのように自分のテーマのものとして書き換えます。
 
OK!NEXT!


functions.php

とくにやることなし!
OK!NEXT!


index.php

index.phpにはちょっと仕掛けが必要です。

  • yiiの公開ディレクトリを変更したための書き換え
  • WordPressがmagic_quotesを使うことを前提としている

特に後者が厄介で、PHP5.3では非推奨となっていますが、WordPressのお作法としては必須で、
WordPress自前のmagic_quotesがグローバルに影響を与えています。
(この辺が WordPress ≠PHPなわけなんですが)
 
というわけで、これを回避しましょう。

// comment out the following two lines when deployed to production
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');

require(__DIR__ . '/vendor/autoload.php');
require(__DIR__ . '/vendor/yiisoft/yii2/Yii.php');

$config = require(__DIR__ . '/config/web.php');

//wordpressのマジッククォーツを回避
$_GET = stripslashes_deep($_GET);
$_POST = stripslashes_deep($_POST);
//stripslashes_deep($_REQUEST);//リクエスト全体も必要な場合。

(new yii\web\Application($config))->run();

このように書き換えます。

OK!NEXT!


config/web.php

Yii2の設定ファイルを変更します。
 
まず、$config[‘components’]に

        //ショートカットURL
        'urlManager' => [
            'enablePrettyUrl' => true,
            'showScriptName' => false,
            'enableStrictParsing' => false,
            'rules' => [
                '' => 'wordpress/index',
            ],
        ],

を追加。デフォルトインストール状態では、Yii2はPrettyUrlになっていませんので、これを有効にします。
また、一旦どんなURLだろうとも、wordpressというYii2のコントローラーにいれてしまう設定をします。
これについては後述。
 
次に、同じく$config[‘components’]に

        //アセットの場所を設定
        'assetManager' => [
            'basePath' => get_template_directory() . '/assets/',
            'baseUrl' => get_template_directory_uri() . '/assets/',
        ],

を設定します。
これは、Yiiが認識するwebrootと、実際のYiiのインストールディレクトリにズレがあるのを修正します。
 
次、$config[‘components’][‘request’]に

    'enableCsrfValidation' => false,

を設定します。いまのところまだ研究中なんですが、Yii2のCSRF対策がうまく効かなくなるので、自動化は中止します。
必要であれば自前のヘルパを用意するのがよいでしょう。
 
OK!NEXT!


config/db.php

データベース設定です。これはWPの設定を引き継ぎたいところです。

return [
    'class' => 'yii\db\Connection',
    'dsn' => 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=' . DB_CHARSET,
    'username' => DB_USER,
    'password' => DB_PASSWORD,
    'charset' => 'utf8',
	'tablePrefix' => $wpdb->prefix
];

このように設定します。WPの定数とプリフィックスを取得して設定すればOK。

OK!これで設定は完了です。
次に必要なのはコントローラーの設置です。


controllers/WordpressController.php

controllersフォルダの中にWordpressController.phpを作成し

namespace app\controllers;
use Yii;

class WordPressController extends \yii\web\Controller
{
    public function actionIndex()
    {
		//ルーティングを書き換える
		$route = $this->checkMethod();
		if( $route !== FALSE )
		{
			return Yii::$app->runAction($route);
		}
		return $this->wp();
    }
	
	// ----------------------------------------------------
	/**
	 * WP用の外出し
	 */
	protected function wp()
	{
		
		if ( is_robots() ) {
			do_action('do_robots');
			return;
		}
		else if ( is_feed() ) {
			do_feed();
			return;
		}
		else if ( is_trackback() ) {
			include(ABSPATH . 'wp-trackback.php');
			return;
		}
		else if ( is_404() ) {
			return $this->renderPartial('404' );
		}
		else if ( is_search() )
		{
			return $this->renderPartial('search' );
		}
		else if ( is_tax() )
		{
			return $this->renderPartial('taxonomy' );
		}
		else if ( is_home() )
		{
			return $this->renderPartial('index' );
		}
		else if ( is_attachment() )
		{
			//remove_filter('the_content', 'prepend_attachment');
			return $this->renderPartial('attachment' );
		}
		else if ( is_single() )
		{
			return $this->renderPartial('single' );
		}
		else if ( is_page() )
		{
			return $this->renderPartial('page' );
		}
		else if ( is_category() )
		{
			return $this->renderPartial('archive' );
		}
		else if ( is_tag() )
		{
			return $this->renderPartial('archive' );
		}
		else if ( is_author() )
		{
			return $this->renderPartial('author' );
		}
		else if ( is_date() )
		{
			return $this->renderPartial('archive' );
		}
		else if ( is_archive() )
		{
			return $this->renderPartial('archive' );
		}
		else if ( is_comments_popup() )
		{
			return $this->renderPartial('comments_popup' );
		}
		else if ( is_paged() )
		{
			return $this->renderPartial('paged' );
		}
		else
		{
			return $this->renderPartial('index' );
		}
	}
	
	// ----------------------------------------------------
	/**
	 * コントローラーメソッドの存在チェック
	 */
	private function checkMethod()
	{
		$pathInfo = explode( '/', Yii::$app->request->pathInfo );
		$controllerList = [];
		while( count($pathInfo) > 0 )
		{
			$controllerList[] = array_shift( $pathInfo );
			$method = $pathInfo[0];
			$controllerListSet = $controllerList;
			$contorollerName = array_pop($controllerListSet);
			$contorollerName = ucfirst( $contorollerName ) . 'Controller';
			
			$controllerFile = 
				Yii::getAlias('@app')
				. '/controllers/'
				. (
					(
						empty($controllerListSet)
					)?'':implode( '/', $controllerListSet ) . '/' 
				  )
				. $contorollerName
				. '.php';
			$method = $pathInfo[0];
			if( $method == '')
			{
				$method = 'index';
			}
			
			if(file_exists($controllerFile))
			{
				$space = 'app\\controllers\\' . $contorollerName;
				$setMethod = 'action' . ucfirst($method);
				$methodExists = method_exists( $space, $setMethod );
				if( $methodExists === true )
				{
					$return = implode( '/', $controllerList ) . '/' . $method;
					return $return;
				}
			}
		}
		return false;
	}
	
	// ----------------------------------------------------

}

と書きます。
ここでは何をやっているかと言いますと、上述のweb.php設定の中で、
いかなるURLであろうとも、一旦wordpressコントローラーのindexアクションを呼ぶように設定しています。
このアクションの中で、WPの投稿であればsingle.phpなどを呼び、そうでなければ、
Yiiのコントローラーアクションを呼ぶという設定をします。
YiiではHMVCというデザインパターンを採用されており、
コントローラアクション内から別のコントローラアクションをコールできるようにしています。
 
そのことを利用して

      URLからYiiのコントローラーアクションがあればHMVCコール
      WPのページならWPのテンプレート

という順番でルーティングを処理するようにしています。
ここのところで少々速度を食われてしまうわけですが…
 
現時点ではGETパラメータをどうするのかという点については、
考慮していません。
ここについてはURL構造の問題ですので、続編をお待ちください。
最低限YiiがWPの中で動くことを想定しています。


ビューファイルをコピー

次に、WPのsingle.phpなどをYiiのビューとしてコピーします。
このとき注意しなくてはならないのは、
single.phpやarchive.phpなどWPから最初にコールされるファイルは
Yiiのビューファイルとして配置。
header.phpなどのテーマファイルの中からコールされるファイルは、
Yiiのルートに置く必要があります。
 
yii2/views/wordpressフォルダを作成し、
その中に

  • 404.php
  • archive.php
  • author-bio.php
  • image.php
  • index.php
  • page.php
  • search.php
  • single.php
  • sidebar.php

をコピー。それ以外のファイルはYiiのindex.phpと並ぶように配置します。
 
よっしゃ次最後!


views/layout/main.php

最後に、Yiiのレイアウトファイルから余分なタグを削ります。
views/layout/main.php

<?php

/* @var $this \yii\web\View */
/* @var $content string */

use yii\helpers\Html;
use yii\bootstrap\Nav;
use yii\bootstrap\NavBar;
use yii\widgets\Breadcrumbs;
use app\assets\AppAsset;

AppAsset::register($this);
?>
<?php $this->beginPage() ?>
    
    
    
    <?= Html::encode($this->title) ?>
    <?php $this->head() ?>
	
	
	<?php $this->beginBody() ?>

isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [], ]) ?>
<?php $this->endBody() ?> <?php $this->endPage() ?>

とします。
 
本来であれば、WP側のheader.phpとの兼ね合いでタイトルを設定したりは必要だと思いますが、
まずはこれだけあれば大丈夫でしょうという動きを。
 


以上で、なんとかWPの中でYii2を動かすという流れでした。
この状態で
http://domain/gii
を動かすとYiiのスカッフォールディングが動きます。


 
▲上記のような画面構成になります。
なんとかWPの中でYiiが動くようになりました。
これで複雑な検索を必要とするページを作ったり、
WPではまかない切れない難しい一覧ページを出力したりできます。
WPのデータベース設定を継承するようにつくられているので、
WPのデータベーステーブルに対してモデルファイルをつくっておけば、
WPの複雑なリレーションに対して簡単なデータ呼び出しも可能になるでしょう。
 
次回は、

  • GETパラメータの処理
  • header.phpの最適化
  • WPのログオン情報を継承してYiiのコントローラにアクセス制限

をやってみます。
 


P.S.
上述の通り、ほとんどのWEBアプリケーションフレームワークは
一旦index.phpが呼ばれる前提になっていますので、
Yii以外でもHMVCがサポートされていれば、
同じアプローチができるとおもいます!
 
出てこい!俺より強いやつ!!

[覚書]Yiiで複数データ・ソースを扱う

今日は完全な覚書です。
 
僕はYiiが大好きです。あ、PHPのフレームワークのことです。
(ぼくにとっては)使いやすい。現在はYii2がベータ版で開発中。
giiというGUIでのCRUDジェネレータがあり、
管理画面を作るにはとっても手軽、かつ、高機能に振る舞ってくれる
CRUD画面をつくってくれます。
 


こういったフレームワークにかならずついてくるのがORMやらARやら。
YiiにももちろんARがあります。
CActiveRecordを継承したクラスをつくれば、それで「はい完成」。
(もちろんテーブル名の設定などは必要ですが)
 
ただし、CActiveRecordは、初期値でフレームワークに設定されるDBへの接続を前提につくられており、複数データソースに接続する場合はちょこっと改造が必要です。
 


複数データソースへの接続

'components'=>array(
	'db'=>array(
		'connectionString' => 'mysql:host=host;dbname=databasename',
		'emulatePrepare' => true,
		'enableParamLogging' => true,
		'username' => 'user',
		'password' => 'password',
		'charset' => 'utf8',
	),
)

初期値ではこんな感じの設定がconfigにあります。
1つのデータソースへの接続ですね。
 
これを…

'components'=>array(
	'db'=>array(
		'connectionString' => 'mysql:host=host;dbname=databasename',
		'emulatePrepare' => true,
		'enableParamLogging' => true,
		'username' => 'user',
		'password' => 'password',
		'charset' => 'utf8',
	),
	'db2'=>array(
		'class' => 'CDbConnection',//←これ追加
		'connectionString' => 'mysql:host=host;dbname=database2',
		'emulatePrepare' => true,
		'enableParamLogging' => true,
		'username' => 'user',
		'password' => 'password',
		'charset' => 'utf8',
	),
)

こんな風にすると、2つめのデータソースへは

Yii::app()->db2

でアクセス可能です。おぉ。便利。


ARで利用

class TableModel extends CActiveRecord
{
	/**
	 * テーブル名を指定
	 * @return string the associated database table name
	 */
	public function tableName()
	{
		return 'tablename';
	}
	
}

YiiのActiveRecordは最低限これだけ書けばARクラスをつくることができます。
ただ、前述のとおり、Yiiのアクティブレコードは初期で設定されているYii::app()->dbを前提としていますので、2つめのデータソースのAR作成は、ちょこっと変更が必要。
 
そのやり方が…

class DB2Model extends CActiveRecord
{
	
	/**
	 * データ接続先を設定
	 */
	public function getDbConnection()
	{
		return Yii::app()->db2;
	}
	
	/**
	 * テーブル名
	 * @return string the associated database table name
	 */
	public function tableName()
	{
		return 'sessions';
	}
	
}

こんな感じgetDbConnection()をオーバーロードして、
configで追加した別のデータソースをリターンするだけ。
 
おお。さらに便利。


 
誰得です。現場からは以上です。

YiiでInter-Mediatorを使ってみる

INTER-MediatorというPHPアプリケーションフレームワークがあります。
HTMLからのDBカラム指定でデータが呼び出せたり、
Javascriptの制御でDB操作ができたりなど、なかなか重宝するフレームワークです。

が、必ずしも万能というわけではないのかなというところがあり、
私はSymfonyなどに代表されるMVCフレームワークにINTER-MEdiator(以下、IMと略す)を統合して、
利用しています。

以前はCodeigniterにIMを統合するという記事を書きましたが、今回はYiiに統合をしてみます。


Yii?

PHPのフレームワークといえば?というところで最初にあがってくるようなフレームワークではないですが、柔軟で高速で、かつ、スカッフォールディングなどベースの生成が頭がいいので、気に入って利用しています。他方CakePHPほどの「なんでも感」はないので、PHPerとしての「地力」が試されるフレームワークであると思っています。


早速やってみましょう

これよりさきはMySQLへの接続を前提としてお話をすすめます。

IMのファイルは、yiiのアプリケーションディレクトリ(通常であればprotected)に配置します。

/protected/Inter-Mediator/...

のような感じですね。
特に何かコマンドを打つ必要はありません。


設定ファイルの書き換え

データベースへの接続設定をIMでも使いまわせるようにするために、
初期値ではメイン設定に閉じ込められているデータベース設定を、
別ファイルに移します。

/protected/config/main.phpの

'db'=> array(
			'connectionString' => 'mysql:host=localhost;dbname=dbname',
			'emulatePrepare' => true,
			'username' => 'user',
			'password' => 'pass',
			'charset' => 'utf8',
		), 

の値の部分をカットして、

'db'=> require dirname(__FILE__) . '/database.php'

とします。新規に/protected/config/database.phpを作成して、その中を

<??php
return array(
			'connectionString' => 'mysql:host=localhost;dbname=dbname',
			'emulatePrepare' => true,
			'username' => 'user',
			'password' => 'password',
			'charset' => 'utf8',
		);

とします。
さらに、main.phpのほうには、Yiiのセッション機能を使えるようにするために、加筆をします。
(IMの設定情報をセッションで保持することで柔軟に利用できるようにします)

//DBを外に
		'db'=> require dirname(__FILE__) . '/database.php',
		
		
		//セッションを追加
		'session'=>array(//追加
				'class'=>'CDbHttpSession',//追加
				'sessionTableName'=>'sessions',//追加
				'connectionID'=>'db',//追加
		),

先ほどのDBの設定の部分の下に、セッションを利用可能にしておきます。
そして、下記に説明するヘルパを自動読み込みさせるために、サードパーティーライブラリのフォルダを自動ロードするように設定します。

	'import'=>array(
		'application.models.*',
		'application.components.*',
	),

とします。

	'import'=>array(
		'application.models.*',
		'application.components.*',
		'application.vendor.*',
	),

に書き換えます。
設定ファイルの書き換えは以上。


ヘルパファイルの作成

続いて、IMの設定をラクにするヘルパファイルを作ります。
/protected/vendor/ImHelper.php
を作成し、

<??php
YiiBase::import('application.Inter-Mediator.*');
class ImHelper
{
	//デバッグ
	private static $debug = FALSE;
	
	// ----------------------------------------------------
	
	/**
	 * IMコードのセット
	 */
	public static function setImCode( $im1, $im2 = array(), $im3 = array() )
	{
		//データベース設定をロードしてIMの引数に設定
		$database = require YiiBase::getPathOfAlias('application.config') . '/database.php';
		$im3['db-class'] = 'PDO';
		$im3['dsn'] = $database['connectionString'];
		$im3['user'] = $database['username'];
		$im3['password'] = $database['password'];
		
		
		//一時コードの発行
		$code = sha1( uniqid( '', true ) );
		$data = array(
			'im1' => $im1,
			'im2' => $im2,
			'im3' => $im3,
			'debug' => self::$debug
		);
		Yii::app()->session['im_temporary_code_' . $code] = $data;
		
		return $code;
	}
	
	// ----------------------------------------------------
	
	/**
	 * IMコードの取得
	 * @param type $im 
	 */
	
	public static function getImCode( $code )
	{
		global $callURL;
		$callURL = Yii::app()->createAbsoluteUrl( 'im/get', array('code'=>$code) );
		require_once 'INTER-Mediator.php';
		$params = Yii::app()->session['im_temporary_code_' . $code];
		//echo 'var test = ' . json_encode($params) . ';';
		IM_Entry( $params['im1'], $params['im2'],  $params['im3'], $params['debug'] );
	}
	
	// ----------------------------------------------------
	
	/**
	 * URL作成
	 */
	public static function getUrl( $im1, $im2 = array(), $im3 = array() )
	{
		$code = self::setImCode($im1, $im2, $im3);
		return Yii::app()->createAbsoluteUrl( 'im/get', array('code'=>$code) );
	}
	
	// ----------------------------------------------------
}

と書いて起きます。

YiiBase::import('application.Inter-Mediator.*');

で、IMのフォルダ内をオートロードの対象にしておきます。


IM自体の改造

IM自体の改造が必要です。

まず、INTER-Mediator.phpを改造します。

if (!class_exists('Crypt_RSA')) {
    require_once($currentDir . 'phpseclib' . DIRECTORY_SEPARATOR . 'Crypt' . DIRECTORY_SEPARATOR . 'RSA.php');
}
if (!class_exists('Crypt_Hash')) {
    require_once($currentDir . 'phpseclib' . DIRECTORY_SEPARATOR . 'Crypt' . DIRECTORY_SEPARATOR . 'Hash.php');
}
if (!class_exists('Math_BigInteger')) {
    require_once($currentDir . 'phpseclib' . DIRECTORY_SEPARATOR . 'Math' . DIRECTORY_SEPARATOR . 'BigInteger.php');
}

//Yiiのオートローダーを一旦解除
spl_autoload_unregister(array('YiiBase', 'autoload'));


if (!class_exists('Crypt_RSA')) {
    require_once($currentDir . 'phpseclib' . DIRECTORY_SEPARATOR . 'Crypt' . DIRECTORY_SEPARATOR . 'RSA.php');
}
if (!class_exists('Crypt_Hash')) {
    require_once($currentDir . 'phpseclib' . DIRECTORY_SEPARATOR . 'Crypt' . DIRECTORY_SEPARATOR . 'Hash.php');
}
if (!class_exists('Math_BigInteger')) {
    require_once($currentDir . 'phpseclib' . DIRECTORY_SEPARATOR . 'Math' . DIRECTORY_SEPARATOR . 'BigInteger.php');
}

//Yiiの再登録
spl_autoload_register(array('YiiBase', 'autoload'));

とします。これは、先のImHerlper.phpでIMのディレクトリ内をオートロードの対象にしたわけですが、PHP拡張である「Crypt_RSA」などのクラスの存在拡張をチェックすると、自動的にファイルを探しに知ってしまうのを回避しています。

続いて、GenerateJSCode.phpを改造します。

public function generateInitialJSCode($datasource, $options, $dbspecification, $debug)
{
	$q = '"';

public function generateInitialJSCode($datasource, $options, $dbspecification, $debug)
{
	global $callURL;
	$q = '"';

としておきます。IMのコードを見ると、$callURLがセットされていれば、このURLを優先して利用する仕組みになっているようですので、globalにして、先のImHelper.php内でコールするURLを指定できるように改造しています。

もしかすると、

if (isset($callURL)) {

if ( $callURL != '' ){

にしておいたほうが賢明かもしれません。


IM処理用コントローラーの設置

次にIMの処理を担うコントローラーファイルを設置します。
/protected/controllers/ImController.php
を作成し

<??php
class ImController extends CController
{
	
	// ----------------------------------------------------
	
	/**
	 * アクセスルール
	 */
	public function accessRules()
	{
		return array(
				'actions'=>array(  ),
				'users'=>array('@'),
			),
		);
	}
	
	// ----------------------------------------------------
	
	/**
	 * IM用コード出力
	 */
	public function actionGet( $code )
	{
		echo ImHelper::getImCode($code);
	}
	
	// ----------------------------------------------------
}

と保存しておきます。アクセス状況によってはアクセスルールを適切に設定したほうがよいでしょう。


準備完了

準備ができました。
あとは任意のコントローラーアクション内で、

$ds = array(
			array(
				'name' => 'categories',
				'records' => 20,
				'paging' => true,
			),
		);
		$def = array();
		$ds2 = array();
		
		$url = ImHelper::getUrl(
				$ds,
				$def,
				$ds2
		);

とすれば、$urlにIMの設定を呼び出すための一時URLが発行されるため、
viewの中で表示するなり、clientScriptクラスに登録するなりすればOK

ImHelper::getUrl()の引数は
IM_Entry()の引数に引き当てられます。
DBへの接続設定はImHelperで自動的に割り当てられるので、不要です。


これで、開発高速化!

現場からは以上です。

Older posts