備忘録的な @7wk

phpとjavascript系をメインに扱っていく予定。

久々のPHP。そしてLaravelを味見。

久々にPHPを触るのでメモ。

肩慣らしにブログっぽいアプリを作成。

フレームワーク勢力図も変わってるし、
PHP7も使ってみたいのでLaravel5.5を使ってみる。

とりあえず5.4のマニュアルを見つつ詰まったらググる感じで進めてみる。
インストール 5.4 Laravel

まだ正式版はリリースされていないので
dev-developでインストール

$composer create-project --prefer-dist laravel/laravel blog dev-develop
$cd blog
# 一応バージョン確認
$php artisan --version

>Laravel Framework 5.5-dev

# ビルトインサーバーで試してみる
$php artisan serve

>Laravel development server started: <http://127.0.0.1:8000>

これでとりあえず動作の確認はできた。

データベースはfirebaseを使おうかと思ったけど
ローカルで試すのでMySQLで良いかな。
しかしデータベースはすぐ触る予定もないので
必要になり次第入れるつもり。

javascriptcssはlaravel-mixでうまいことやってもらう事にする。
最近npmではなくyarnを使ってるのでyarnで入れる

※laravel-mixの解説はこの辺り
アセットのコンパイル(Laravel Mix) 5.4 Laravel

$yarn
$yarn add laravel-mix cross-env node-sass --dev

変更を検知してコンパイルしてもらう。

$yarn run watch

デフォルトのviewに
コンパイル後のcssとjsを読み込む

vueは更新頻度が低くなるはずなので webpack.mix.jsを一部変更

mix.js('resources/assets/js/app.js', 'public/js')
   .extract(['vue'])
   .sass('resources/assets/sass/app.scss', 'public/css');

んで、デフォルトのテンプレートで試す。
resources/views/welcome.blade.php に追記

<link rel="stylesheet" href="{{ mix('css/app.css') }}">
<script src="{{ mix('js/manifest.js') }}"></script>
<script src="{{ mix('js/vendor.js') }}"></script>
<script src="{{ mix('js/app.js') }}"></script>

この時点でjsのエラーが2つ

app.js:756 CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token
vendor.js:523 [Vue warn]: Cannot find element: #app

直す前にcssフレームワークTwitter bootstrapからbulmaに変更、
jQueryは使わないので削除

$yarn remove bootstrap-sass jquery
$yarn add bulma

resouces/assets/sass/app.sass
を修正

--// Bootstrap
--@import "node_modules/bootstrap-sass/assets/stylesheets/bootstrap";
++@import "node_modules/bulma/sass/utilities/initial-variables";
++@import "node_modules/bulma/bulma";

で、エラーを直す
resources/views/welcome.blade.php に追記

        <meta name="csrf-token" content="{{ csrf_token() }}">

      <div id="app">
        <example>サンプルを動かす</example>
      </div>

これで一旦エラーは回避。  
まだbootstrapとjQueryの残骸があるので削除
resouces/assets/bootstrap.js

try {
    window.$ = window.jQuery = require('jquery');

    require('bootstrap-sass');
} catch (e) {}

これまででvueが動いていてエラーが起きていないのが確認できた。
そして今日はここまで。
気が向いたら続けてく。

fuelphpが1.6になったのでcomposerでパッケージを追加してみた

fuelphp1.6も正式版になったので
composerで何か入れるサンプルでもメモついでに試してみる。
Packagistに公開されていないもので手頃そうな

fuel-language-pack-jaを入れてみます。


composer.jsonの該当箇所に追加

    "repositories":[
        {
            "type": "package",
            "package": {
                "name": "sharkpp/fuel-language-pack-ja",
                "version": "1.0",
                "dist": {
                    "url": "https://github.com/sharkpp/fuel-language-pack-ja/zipball/master",
                    "type": "zip"
                },
                "source": {
                    "url": "https://github.com/sharkpp/fuel-language-pack-ja.git",
                    "type": "git",
                    "reference": "master"
                }
            }
        }
    ],
    "require": {
        "sharkpp/fuel-language-pack-ja": "1.*"
    },

これでコマンドラインから下記のコマンドでインストール完了。

php composer.phar update

あとはconfigを修正

<?php
return array(
    'language' => 'ja',
    'always_load' => array(
        'packages' => array(
            array('fuel-language-pack-ja' => APPPATH.'..'.DS.'vendor'.DS.'sharkpp'.DS.'fuel-language-pack-ja'.DS),
        ),
    ),
);

これでValidationのメッセージが日本語になりました。
composerは楽ちんでステキ。

FuelPHPのUploadクラスを使ってみる

画像の管理をするにあたりファイルアップロードを使うので
FuelPHPのUploadクラスを使ってみる。
ただデータはファイルではなくデータベースに格納します。

マニュアルでいうとこのあたり。
http://fuelphp.jp/docs/1.5/classes/upload/config.html

そしていつものようにエラーチェックは省略しているので、
万が一にここを見て書く人がいましたらちゃんとチェックして下さい。


まずconfigをコピー。

cp core/config/upload.php app/config/


設定ファイルを編集します。
ファイルに保存しないのでアップロードファイルの種類を限定するのみです。
他はデフォルトのまま設定してあります。

<?

return array(
	// 拡張子がpng, jpg, gifのファイルのみ許可
	'ext_whitelist'	 => array('png', 'jpg', 'gif',),
	// ファイルタイプがimageのモノのみ
	'type_whitelist' => array('image',),
	// png,jpg,gifのみ許可
	'mime_whitelist' => array('png', 'jpg', 'gif', 'jpeg', 'pjpeg', 'x-png',),

);


画像データ格納テーブルの作成
元のファイル名と拡張子、縦横を保持します。

CREATE TABLE `pictures` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(64) NOT NULL DEFAULT '',
  `content` blob NOT NULL,
  `ext` varchar(4) NOT NULL DEFAULT '',
  `width` int(11) NOT NULL,
  `height` int(11) NOT NULL,
  `created` datetime NOT NULL,
  `modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

モデルも作成。こちらORMでcreatedは作成日時、
modifiedは更新日時が入るよう設定済みの
\Model\Base_Ormを継承してます。
そしてaddでデータを保存します。

<?php
namespace Model;
class Picture extends \Model\Base_Orm
{
	public static function add($file = array())
	{
		$size = getimagesize($file['file']);
		$data = array(
			'name'    => $file['name'],
			'content' => file_get_contents($file['file']),
			'ext'     => $file['extension'],
			'width'   => $size[0],
			'height'  => $size[1],
		);
		return static::forge($data)->save();
	}
}


Controllerを書きます
自分の環境はがちょっと独自なので誤ってたらすみません。
parserはtwigです。

<?php
namespace Controller;

class Picture extends \Controller
{
	// リスト表示&アップロードフォーム
	public function action_index()
	{
		// リスト表示
		// 処理省略
		$data = array();
		return \Response::forge(\View::forge('picture/index.html', $data));
	}

	// アップロード処理
	public function action_upload()
	{
		// upload
		\Upload::process();
		if (\Upload::is_valid()) {
			$files = \Upload::get_files();
			if ($files) {
				foreach ($files as $file) {
					\Model\Picture::add($file);
				}
			}
		}
		\Response::redirect('picture/index');
	}

	// 画像出力
	public function action_image($id)
	{
		$pic = \Model\Picture::find($id);
		$images = array(
			'png' => 'image/png',
			'jpg' => 'image/jpeg',
			'gif' => 'image/gif',
		);
		$headers = array(
			'Content-type' => $images[$pic->ext],
		);
		return \Response::forge($pic->content, 200, $headers);
	}

}


indexのview

{{ form_open({'action':'picture/upload', 'enctype':"multipart/form-data', 'method':'POST'}) }}
{{ form_file('image') }}
{{ form_submit('', '送信') }}
{{ form_close() }}

一覧表示
> 〜以下略〜

これで画像出力時は

<img src="{{ url('picture/image/' ~ picture.id ~'/image.' ~ picture.ext) }}" width="{{ picture.width }}" height="{{ picture.height }}" />

こんな感じで実装できるかと思います。

以上、今日の備忘録でした。

fuelphpでタブと改行の削除

表示の際、タブと改行は邪魔な場合がほとんどです。
fuelphpで実装する場合、どう実装するのがベストなのかは
いまいちわかりませんがResponseクラスで実装します。

fuel/app/class/response.php
<?php
class Response extends \Fuel\Core\Response {
  public function body($value = false) {
    if ($value === false) {
      return $this;
    }
    if ($this->headers) {
      $this->body = $value;
    } else {
      $this->body = str_replace(array("\t", "\r", "\n"), '', $value);
    }
    return $this;
  }
}

if ($this->headers)で
headerを弄ってる場合は処理しないようにします。
api系が反応して見難くなります。
他にdevの場合処理しないとかで条件を付加してもいいでしょう。

あとはbootstrap.phpで設定をします。

fuel/app/bootstrap.php
<?php
// 該当部分のみ
Autoloader::add_classes(array(
  // Add classes you want to override here
  // Example: 'View' => APPPATH.'classes/view.php',
  'Response' => APPPATH . 'classes/response.php',
));

以上でタブも改行も出力時に削除されるかと思います。
テンプレートエンジンを使っていても大丈夫です。

簡易CMSを作る その2

昨日はCRU(D)を作りましたので、その続きです。

表示部分を作ります。

controllerにaction_detail()を追加します。
例のごとくエラーチェックはなしです。

<?php
public function action_detail($article_id=0)
{
    $article = \Model\Article::forge()->find((int)$article_id);
    $view = \View::forge('article/detail.html');
    if ($article) {
        $view->set('subject', $article->subject);
        $view->set_safe('body', $article->body);
    }
    return \Response::forge($view);
}

viewはいつものtwigです。

article/detail.html

{% extends "layout.html" %}

{% block content %}
    <h1>{{ subject }}</h1>
    <div>
        {{ body|raw|nl2br }}
    </div>
{% endblock %}

以上で完成です。

次に本文入力をWYSIWYGにしてみようと思います。
fuelphpはTwitterBootStrapがデフォルトで組み込まれているので
TwitterBootStrapを使用しているエディタを検索してみます。
何も考えずに「TwitterBootStrap WYSIWYG」でググって最初に出てくるものを使うことにします。
こちらです→bootstrap-wysihtml5

いつもならgitで入れるところですが
そのままcloneすると余計なファイルが多いので
zipで落として必要なファイルのみ使います。
localesを見ると日本語対応しているみたいですね。
すばらしい。

導入も簡単です。
viewに下記を修正・追加です。

<link rel="stylesheet" type="text/css" href="/css/bootstrap-wysihtml5.css"></link>
<script src="/js/wysihtml5-0.3.0.js"></script>
<script src="/js/jquery-1.7.2.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
<script src="/js/bootstrap-wysihtml5.js"></script>
<script src="/js/locales/bootstrap-wysihtml5.ja-JP.js"></script>

〜略〜

{{ form_textarea('body', article.body, {'rows':20, 'id':'body', 'placeholder':'本文を入力して下さい。'}) }}

〜略〜

<script type="text/javascript">
    $('#body').wysihtml5({locale: "ja-JP"});
</script>

これで簡単なCMSができました。
あとはエラー処理・セキュリティ・使い勝手を向上させていく感じでしょうか。
単純な仕組みなので拡張も簡単だと思います。

以上。

簡易CMSを作る その1

RebornCMSはかなり期待したのに
残念な事になっているので
簡単なCMSを実装します。

まずはCRUDが利用できる状態にします。

記事テーブルの作成します。
表題と本文があればいいでしょう。
カテゴリIDはついでです。

CREATE TABLE IF NOT EXISTS `articles` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `category_id` int(11) unsigned NOT NULL DEFAULT '0',
  `subject` varchar(24) NOT NULL,
  `body` text NOT NULL,
  `created_at` datetime NOT NULL DEFAULT  '0000-00-00 00:00:00',
  `modified_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `category_id` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;


modelも作成します。
created_atとmodified_atは自動で更新されるようにしておきます。
が、今回は省略です。

model/article.php

<?php
namespace Model;

class Article extends \Orm\Model
{
}

controllerです。
今回はセキュリティやエラー処理やら使い勝手はパスです。
気が向いたら書きます。

controller/article.php

<?php

namespace Controller;

class Article extends \Controller\Base_Auth
{
	public function action_index()
	{
		$article = \Model\Article::forge();
		$list = $article->find('all');

		return \Response::forge(\View::forge('article/list.html', array('articles' => $list)));
	}

	public function action_edit($article_id = 0)
	{
		$article = \Model\Article::forge();
		$data = $article->find($article_id);

		return \Response::forge(\View::forge('article/edit.html', array('article' => $data)));
	}

	public function action_regist()
	{
		if (\Input::method() == 'POST') {
			$article = \Model\Article::forge();
			if (0 < (INT)\Input::post('id')) {
				$article = $article->find((INT)\Input::post('id'));
			}
			$article->subject = \Input::post('subject');
			$article->body = \Input::post('body');
			$article->save();
		}
		// とりあえずリストへ
		return \Response::redirect('article/index');
	}


}

viewも最低限です。
もちろんtwigを使っています。

article/list.html

{% extends "layout.html" %}

{% block content %}
	<table class="table table-bordered">
		<thead>
			<tr>
				<th></th>
				<th>ID</th>
				<th>Subject</th>
				<th>Body</th>
			</tr>
		</thead>
		<tbody>
			{% for article in articles %}
				<tr>
					<td><a href="{{ url("article/edit/#{ article.id }") }}">編集</a></td>
					<td>{{ article.id }}</td>
					<td>{{ article.subject }}</td>
					<td>{{ article.body }}</td>
				</tr>
			{% endfor %}
		</tbody>
	</table>
	<a href="{{ url("article/edit") }}">新規作成</a>
{% endblock %}

article/edit.html

{% extends "layout.html" %}

{% block content %}
	{{ form_open('article/regist') }}
		{{ form_hidden('id', article.id) }}
		<table class="table table-bordered">
			<thead>
			</thead>
			<tbody>
				<tr>
					<td>ID</td>
					<td>
						{{ article.id }}
					</td>
				</tr>
				<tr>
					<td>Subject</td>
					<td>{{ form_input('subject', article.subject) }}</td>
				</tr>
				<tr>
					<td>Body</td>
					<td>{{ form_textarea('body', article.body, {'rows':20, 'class':'xxlarge'}) }}</td>
				</tr>
			</tbody>
		</table>
	{{ form_button('submit', '登録', {'type':'submit','class':'btn btn-primary'}) }}
	{{ form_close }}
{% endblock %}


これでCRUDのD以外ができました。Dは省略。
今回はこれにて終了。

地震起きなくて良かった。

fuel-nestedsetsを試す。の続き。

昨日は動作確認ができたので、
続けて実用ができるように進めていきます。

まずツリー構造全体を確認したいと思いますが、
READMEを見る限りそれっぽいメソッド名は見つかりません。
コードを読むより書いた方が早いので
確認できるよう下記のコードを書きます。

<?php
$tree = \Model\Tree::forge();
// ツリーの最上位のリストを取得
$parents = $tree->find('all', array('where'=>array('left_id'=>1)));
foreach ($parents as $parent)
{
	echo $parent->tree_get_root()->tree_dump_as_html(array('id'), false);
}

昨日作ったプログラムを2回実行した後、
これを実行すると下記のような結果が表示されます。

1
    2
        3
        4
5
    6
        7
        8


次に、現在はIDのみを表示しているので
名前を付けたいと思います。

とその前に、treesなんてテーブル名を付けてしまったため
idとtree_idがわかりにくいです。
なので名称を変更します。
ツリー構造で思いつくのがカテゴリ管理なので、
categoriesにします。
そしてカテゴリ名を保存する為、
nameフィールドを追加して適当に名前を付けます。

RENAME TABLE  `trees` TO  `categories`;
ALTER TABLE  `categories` ADD  `name` VARCHAR( 24 ) NOT NULL AFTER  `id`;
UPDATE `categories` SET `name` = 'ルート1' WHERE `categories`.`id` =1;
UPDATE `categories` SET `name` = 'カテゴリ1' WHERE `categories`.`id` =2;
UPDATE `categories` SET `name` = 'サブカテゴリ1' WHERE `categories`.`id` =3;
UPDATE `categories` SET `name` = 'サブカテゴリ2' WHERE `categories`.`id` =4;
UPDATE `categories` SET `name` = 'ルート2' WHERE `categories`.`id` =5;
UPDATE `categories` SET `name` = 'カテゴリ2' WHERE `categories`.`id` =6;
UPDATE `categories` SET `name` = 'サブカテゴリ3' WHERE `categories`.`id` =7;
UPDATE `categories` SET `name` = 'サブカテゴリ4' WHERE `categories`.`id` =8;

モデルも同時に修正します。
今回はnameを追加したので
title_fieldにnameを設定です。

<?php

namespace Model;

class Category extends \Nestedsets\Model {

    public static $tree = array(
        'left_field'     => 'left_id',      // name of the tree node left index field
        'right_field'    => 'right_id',     // name of the tree node right index field
        'tree_field'     => 'tree_id',           // name of the tree node tree index field
        'tree_value'     => 0,           // value of the selected tree index
        'title_field'    => 'name',           // value of the tree node title field
        'symlink_field'  => 'symlink_id',   // name of the tree node tree index field
        'use_symlinks'   => false,          // use tree symlinks?
    );

}


ツリー構造を確認するのに
tree_dump_as_htmlのIDを設定している配列にnameを追加します。
これで下記のようになります。

1 "ルート1"
    2 "カテゴリ1"
        3 "サブカテゴリ1"
        4 "サブカテゴリ2"
5 "ルート2"
    6 "カテゴリ2"
        7 "サブカテゴリ3"
        8 "サブカテゴリ4"


最後にテンプレート側で使えるようにし、
tree_dump_as_htmlと同等の表示をします。

コントローラーを下記のように修正。

<?php
\Package::load('fuel-nestedsets');
$tree = \Model\Category::forge();
$parents = $tree->find('all', array('where'=>array('left_id'=>1)));
$categories = array();
foreach ($parents as $parent)
{
	$categories[] = $parent->tree_get_root()->tree_dump_as_array(array(), false);
}

return \Response::forge(\View::forge('member/category.html', array('categories' => $categories)));

テンプレートを作成。
自分の場合はtwigを使っているので下記のようになっています。

{% extends "layout.html" %}

{% block content %}
		{% for tree in categories %}
			{% for category in tree %}
				{% for i in 0..category._level_ %}  {% endfor %}
				{{ category.id }}:{{ category.name }}<br />
			{% endfor %}
		{% endfor %}
{% endblock %}

それでアクセスすると

  	 1:ルート1
    	 2:カテゴリ1
      	 3:サブカテゴリ1
      	 4:サブカテゴリ2
  	 5:ルート2
    	 6:カテゴリ2
      	 7:サブカテゴリ3
      	 8:サブカテゴリ4

こんな感じで表示されます。

あとは追加・修正・削除・移動を実装すればいいのですが、
これは既存のメソッドをラップするだけで可能すね。

というわけで今日はここまで。
次回は別の事書く予定です。