あいどる💖たいむ

あいどるやってます。

Phalcon ACLによるアクセスコントロール

ACLでアクセスコントロールを行うメモ

環境

Phalcon 3.2.2

実装

InvoチュートリアルのACLのコードを元に自分のサービス用に改変している。

Tutorials : Invo - Phalcon Framework

デフォルト設定

setDefaultActionメソッドで、デフォルトはすべてのアクセスを禁止する。

use Phalcon\Acl;
use Phalcon\Acl\Role;
use Phalcon\Acl\Resource;
use Phalcon\Acl\Adapter\Memory as AclList;

$acl = new AclList();

$acl->setDefaultAction(Acl::DENY);

ロール

以下の3つのロールを作成する。

ロール 備考
Guest ゲスト用ロール
User ユーザ用ロール
Admin 管理者用ロール、Userを継承
// ロールの作成
$userRole = new Role('Users');
$adminRole = new Role('Admin');
$guestRole = new Role('Guest');
$roles = [$userRole, $adminRole, $guestRole];

// aclにロールを登録
$acl->addRole($userRole);
$acl->addRole($guestRole);
$acl->addRole($adminRole, $userRole); // 第2引数に指定したロールを継承できる。

リソース

Invoのチュートリアルでは、一つ一つのアクション名をリストに追加していたが、コントローラ別で設定したい場合もあると思う。 そのようなときのために、$ALL_ACTIONS定数を用意した。

// リソースのすべてのアクセスを許可するための文字列
static $ALL_ACTIONS = '_all_actions';

$resources = [
    'index' => [$ALL_ACTIONS],
    'items' => [$ALL_ACTIONS],
    'accounts' => ['index', 'login', 'signUp', 'manage'],
];

foreach ($resources as $resource => $actions) {
    $acl->addResource(new Resource($resource), $actions);
}

アクセス権の設定

次のようなアクセス権を設定する。

リソース 各ロールに対する設定内容
index すべてのロールで、すべてのアクセスを許可
items すべてのロールで、すべてのアクセスを許可
accounts Adminロールはすべてのアクセス許可、Usersは"manage"以外を許可、Guestはすべて禁止
// 全ロールに許可するリソースはまとめて許可
$publicResources = ['items', 'index'];
foreach ($roles as $role) {
    foreach ($publicResources as $resource) {
        foreach ($resources[$resource] as $action) {
            $acl->allow($role->getName(), $resource, $action);
        }
    }
}

// ロール別の許可設定
$acl->allow('Users', 'accounts', ['index', 'login', 'signUp']);
// Adminにmanageを追加(AdminはUsersを継承しているので、すでに1行上でUsersに許可しているindexなどの設定は不要)
$acl->allow('Admin', 'accounts', ['manage']);

動作確認

isAllowedメソッドを呼び出し、確認する。

var_dump($acl->isAllowed('Guest', 'index', $ALL_ACTIONS)); // true
var_dump($acl->isAllowed('Guest', 'accounts', 'index')); // false

var_dump($acl->isAllowed('Users', 'accounts', 'index')); // true
var_dump($acl->isAllowed('Users', 'accounts', 'manage')); // false

// AdminロールはUsersロールを継承しているので’accounts/index'のアクセスも可能
var_dump($acl->isAllowed('Admin', 'accounts', 'index')); // true
var_dump($acl->isAllowed('Admin', 'accounts', 'manage')); // true

実際のアプリケーションでの使用はInvoのように、{リソース名=コントローラ名、アクセス名=アクション名}のアクセス権があるか確認するようにして使うが、$ALL_ACTIONSが許可されているかもORで調べる。

$role = 'Users';
$controller = 'items';
$action = 'index';

$isAllowed =
    $acl->isAllowed($role, $controller, $action)
    || $acl->isAllowed($role, $controller, $ALL_ACTIONS);
var_dump($isAllowed);

全体ソース

phalcon_acl_example.php · GitHub

参考

Phalcon model find メモ

主キー検索

findFirstメソッドの引数にキー値を渡す。

Hoge::findFirst("1");

主キーが複合キーのときは、

Hoge::find("ham=1 and spam=1");

のようにするとか、下記conditionを使う。

1つのカラムの値を指定した検索

<モデル>::findBy<プロパティ名>(<検索値>)のように検索できる。

Users::findById(4); // id = 4のユーザを検索
Users::findBySex('male'); // sex = `male`のユーザを検索

オプションを使ったfindメソッド

findの引数に配列を渡すことで、いろんな検索が可能。

$a = Users::find(
    [
        'conditions' => 'name = ?1',
        'bind' => [
            1 => 'spam',
        ]
    ]
);

?1はプレースホルダで,直下にあるbindで値を指定する。
また、引数の配列の1つ目は、条件とみなされるので、conditionsは明示的に記述しなくてもOK。(個人的にはあったほうが分かりやすいので必ず書くことにする)

var_dump($a->count());
$a = Users::find(
    [
        'name = ?1',
        'bind' => [
            1 => 'spam',
        ]
    ]
);

プレースホルダは文字列も使える。その場合は:<プレースホルダ名>:とし、bindのキーは文字列"<プレースホルダ名>"とする。

$a = Users::find(
    [
        'conditions' => 'name = ?1 and id= :id:',
        'bind' => [
            1 => 'aaa',
            'id' => 4
        ]
    ]
);

オプションで、group byやorder byをかけることもできるので、以下のリンクにある表を参考に組み立てる。

docs.phalconphp.com

Criteriaを使った検索

findを使った検索よりSQLに近い感じで書けるので、こっちのほうが好き。 query()メソッドでPhalcon\Mvc\Model\Criteriaオブジェクトが返ってくるので、これのメソッドを呼び出して、SQLを組み立てるイメージ。

$a = Users::query()
    ->where('sex=?1')
    ->orderBy('name')
    ->bind([1 => 'female'])
    ->execute();

left joinなどもできるので、以下を参考に実装する。

docs.phalconphp.com

おわりに

個人では、簡単な検索はfind使って、 複数条件指定やソートなどがあるときはCriteriaを使った検索を使うようにしたい。

Phalcon DevToolsでsnake caseの列名をcamel caseにしたモデルを生成する

テーブルの列名はsnake caseだけど、モデルの変数名はcamel caseにしたい。 そんなときのtips。

テーブル

CREATE TABLE items
(
  id      INT AUTO_INCREMENT PRIMARY KEY,
  title   VARCHAR(255) NULL,
  body    TEXT         NULL,
  user_id INT          NULL
);

このテーブルのモデルでは、user_idはuserIdという変数名でする。

Phalcon DevTools

versionは3.2.3

modelコマンドに--camelizeと--mapcolumnをつける。 --camelizeはcamel caseにするオプション。
`--mapcolumnはcolumnMapをモデルに持たせるオプション。

phalcon model items --camelize --mapcolumn

生成されたモデル

<?php

class Items extends \Phalcon\Mvc\Model
{

    /**
     *
     * @var integer
     * @Primary
     * @Identity
     * @Column(type="integer", length=11, nullable=false)
     */
    public $id;

    /**
     *
     * @var string
     * @Column(type="string", length=255, nullable=true)
     */
    public $title;

    /**
     *
     * @var string
     * @Column(type="string", nullable=true)
     */
    public $body;

    /**
     *
     * @var integer
     * @Column(type="integer", length=11, nullable=true)
     */
    public $userId;
  
// 〜〜〜〜〜〜〜 中略 〜〜〜〜〜〜〜〜

    /**
     * Independent Column Mapping.
     * Keys are the real names in the table and the values their names in the application
     *
     * @return array
     */
    public function columnMap()
    {
        return [
            'id' => 'id',
            'title' => 'title',
            'body' => 'body',
            'user_id' => 'userId'
        ];
    }

}

テーブルの変数名とモデルの列名が異なる場合は、columnMapで対応づけするが、これを自動で書き込んでくれている。

Phalconでのデータ登録の流れ

概要

ユーザの登録を想定して、Phalconのデータ登録の流れをみる。 Phalconプロジェクト名は”todo"としている。

環境

  • phalcon 3.2.2
  • phalcon-devtools 3.2.3
  • MySQL 5.7.19

モデル作成

まず、DBにモデルのテーブルを作成する。
その後、Phalcon-toolsを使い、作成したテーブルのモデルクラスを作成する。 (Djangoのように、先にモデルクラスを作って、そこからテーブルを作成するようなのはできなさげ)

最初に、ユーザを管理するusersテーブルを作成する。

CREATE TABLE users (
  name     VARCHAR(255) PRIMARY KEY ,
  password VARCHAR(255)
);

つぎに、Phalcon-toolsを使いusersモデルクラスを生成する。 phalcon modelを実行するが、実行前にアプリケーションルート(todo/)に移動しておく。

# phalcon model users

Phalcon DevTools (3.2.3)


  Success: Model "Users" was successfully created.

これで、todo/app/models/にUsers.phpが生成される。

サインアップページの作成

コントローラ作成

モデルと同じようにPhalcon-toolsを使って、accountsコントローラを作成する。

# phalcon controller --name accounts

Phalcon DevTools (3.2.3)


  Success: Controller "accounts" was successfully created.

/var/www/html/todo/app/controllers/AccountsController.php

app/controllers/AccountsController.phpが作成されたので、これをいじっていく。

フォーム追加

コントローラ編集前に、サインアップページに表示するフォームを追加する。

まず、フォームを配置するディレクトリtodo/app/formsを作成。
このディレクトリをconfig.php, loader.phpにロード対象として追加する。 (ここconfigで登録したいディレクトリだけ別のリストに保持するとかすれば、loader.phpは変更しなくてよくできそう)

// config.php
    'application' => [
        'appDir'         => APP_PATH . '/',
        'controllersDir' => APP_PATH . '/controllers/',
        'modelsDir'      => APP_PATH . '/models/',
        'migrationsDir'  => APP_PATH . '/migrations/',
        'viewsDir'       => APP_PATH . '/views/',
        'pluginsDir'     => APP_PATH . '/plugins/',
        'libraryDir'     => APP_PATH . '/library/',
        'cacheDir'       => BASE_PATH . '/cache/',
        'formsDir'       => APP_PATH . '/forms/', // <- 追加
// loader.php

$loader->registerDirs(
    [
        $config->application->controllersDir,
        $config->application->modelsDir,
        $config->application->formsDir // <- 追加
    ]
)->register();

次にSignUpFormクラスを作成する。

<?php

use Phalcon\Forms\Form;
use Phalcon\Forms\Element\Text;
use Phalcon\Forms\Element\Password;
use Phalcon\Validation\Validator\PresenceOf;
use Phalcon\Validation\Validator\StringLength;
use Phalcon\Validation\Validator\Confirmation;

/**
 * SignUpフォーム
 */
class SignUpForm extends Form
{

    /**
     * フォーム初期化
     *
     * @access public
     *
     * @param Users $entity
     * @param array $options
     */
    public function initialize($entity = null, $options = array())
    {
        $name = new Text('name');
        $name->setLabel('name');
        $name->addValidators(
            [
                new PresenceOf(
                    [
                        'message' => 'nameを入力してください',
                    ]
                ),
                new StringLength(
                    [
                        "max" => 255,
                        "messageMaximum" => "nameは255文字以下で入力してください",
                    ]
                )
            ]
        );
        $this->add($name);

        $password = new Password('password');
        $password->setLabel('パスワード');
        $password->addValidators(
            [
                new PresenceOf(
                    [
                        'message' => 'パスワードを入力してください',
                    ]
                ),
                new StringLength(
                    [
                        'max' => 255,
                        'messageMaximum' => 'パスワードは255文字以下で入力してください',
                    ]
                ),
                new Confirmation(
                    [
                        'with' => 'password2',
                        'message' => 'パスワードが一致していません'
                    ]
                )
            ]
        );
        $this->add($password);

        $password2 = new Password('password2');
        $password2->setLabel('パスワード(確認用)');
        $password2->addValidators(
            [
                new PresenceOf(
                    [
                        'message' => 'パスワード(確認用)を入力してください',
                    ]
                ),
            ]
        );
        $this->add($password2);
    }
}

こんな感じ。
validationについては以下を参照。 docs.phalconphp.com

アクション追加

生成されたコントローラにsignUpActionを追加 上で作成したフォームを指定している。

    public function signUpAction(){
        # 使用するフォームを指定
        $this->view->form = new SignUpForm();
    }

ビュー追加

todo/views/accounts/signUp.viewを作成する。 {% for element in form %} ...のような感じでやれば、上で作成したフォームをレンダリングできる。

<h1 class="text-center">アカウント登録</h1>
{{ content() }}
<div class="form-container">
    {{ form("", 'role': 'form','class':'form-horizontal') }}

    <fieldset>
        {% for element in form %}
            <div class="form-group">
                {{ element.label(['class': 'col-sm-3 control-label']) }}
                <div class="col-sm-9">
                    {{ element.render(['class': 'form-control']) }}
                </div>
            </div>
        {% endfor %}
    </fieldset>
    <div class="row">
        <div class="col-sm-offset-3 col-sm-9 ">
            {{ submit_button("登録", "class": "btn-wide btn btn-primary") }}
        </div>
    </div>
    {{ end_form() }}
</div>

{{ content() }}は、viewが階層になっているとき、子viewをここにレンダリングするよってもの。 このviewは最下層だけど、flashメッセージを表示するために記述している。
flashメッセージは階層の一番下に付け足されるイメージなのかな?

階層については以下を参照。

Using Views — Phalcon 3.1.1 documentation (English)

画面確認

http://<host名>/todo/accounts/signUpにアクセス

f:id:shiccocsan:20171019162821p:plain

登録処理の実装

signUpActionを、postのときは登録処理を行うよう変更する。

    public function signUpAction()
    {
        if ($this->request->ispost()) {

            $form = new SignUpForm();
            $user = new Users();

            $data = $this->request->getpost();

            // formのvalidation
            $errors = null;
            if (!$form->isvalid($data, $user)) {
                $errors = $form->getmessages();
            } else {
                // データ登録(validation含む)
                if ($user->create() == false) {
                    $errors = $user->getmessages();
                }
            }


            if ($errors !== null) {
                $e = [];
                foreach ($errors as $error) {
                    $this->flash->error($error);
                }
            } else {
                $form->clear();
                $this->flash->success("ユーザを登録しました");
                return $this->dispatcher->forward(['controller' => 'index', 'action' => 'index']);
            }

        }

        # 使用するフォームを指定
        $this->view->form = new SignUpForm();
    }

formのisvalidメソッドにrequestデータとモデルを渡すことで、validationと一緒にモデルにpostデータもバインドしてくれる。 また、モデルのcreateやsaveメソッドはモデルに設定されているvalidation後に更新処理を行う。

一意制約違反のメッセージ変更

UsersモデルのgetMessageをオーバーライドし、一意制約違反のメッセージを変更する。

    public function getMessages()
    {
        $messages = array();
        // 一意制約違反のメッセージのみ書き換えている
        foreach (parent::getMessages() as $message) {
            switch ($message->getType()) {
                case 'InvalidCreateAttempt':
                    $messages[] = 'この名前はすでに登録されています';
                    break;
                default:
                    $messages[] = $message;
            }
        }
        return $messages;
    }

not null制約や外部キー制約違反などのメッセージ変更できるので、詳しくは以下を参照。

docs.phalconphp.com

動作確認

登録画面で適当に入力し、登録ボタンをクリック。 usersテーブルにデータが登録され、http://<host名>/todo/に遷移できればOK。

おわりに

練習でtodoアプリを作ってるので、その過程を残せたらと思ってたけど、予想以上に書くのが大変だった。 今後は、tipsみたいなのを書いていけたらと思う。

Phalconでプロジェクト作成

phalcon, phalcon DevToolsはインストール済みの前提ですすめる。 

環境

  • phalcon v3.2.2
  • phalcon-devtools v3.2.3

プロジェクト作成

プロジェクトのタイプは4つ選択肢があるが、今回はsimpleとした。

phalcon project --name todo --type=simple

Success: Project 'todo' was successfully created.と出力された後、 http://localhost:8000/todo/(*) にアクセスし、Congratulations!って表示されていればOK.

(※URLは開発環境にあわせて変更する。)

Phalconの処理の流れとしては、/public/index.phpが最初に読み込まれ、そこで記述されたrouterやloaderがインクルードされた後、routerの設定からコントローラを選択し、呼び出す感じでしょうか。 正直ここはあまり理解できてない。 

プロジェクト設定

データベースの接続先を変更する。

todo/app/config/config.phpを開き、\Phalcon\Configのdatabaseの値をDB環境に合わせて変更する。

    'database' => [
        'adapter'     => 'Mysql',
        'host'        => getenv('MYSQL_PORT_3306_TCP_ADDR'),
        'username'    => 'root',
        'password'    => 'root',
        'dbname'      => 'todo',
        'charset'     => 'utf8',
    ],

Libresonic on Docker

LibresonicのDockerイメージがあったので、これを使ってサーバを建てる。

https://hub.docker.com/r/linuxserver/libresonic/

環境

Ubuntu 16.04.1

Dockerコンテナ作成

linuxserver/libresonic(https://hub.docker.com/r/linuxserver/libresonic/) のUsageを参考にコンテナを作成する。

$ lib_home=$HOME/libresonic/

$ docker create \
    --name="libresonic" \
    -v $lib_home/config:/config \
    -v $lib_home/music:/music \
    -v $lib_home/playlists:/playlists \
    -v $lib_home/podcasts:/podcasts \
    -e TZ=Asia/Tokyo \
    -p 4040:4040 \
    linuxserver/libresonic

    # -v $lib_home/other media:/media \
    # -e PGID=<gid> -e PUID=<uid> \
    # -e CONTEXT_PATH=<url-base> \
    
$ docker start libresonic

サーバの起動確認

$ curl -L localhost:4040 | grep -i libresonic
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100  4644  100  4644    0     0   340k      0 --:--:-- --:--:-- --:--:--  340k
<title>Libresonic</title>

タイトルが取得できてるのでOK。

nginxの設定

リクエストをlocalhost:4040にふるために、ホストのリバースプロキシの設定を行う。

server {
    listen       80;
    server_name  <** libresonic.your.server **>; # サーバ名

    location / {
        proxy_pass http://localhost:4040;
   
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

外部からアクセス

外部からアクセスし、Libresonicのトップページが表示されれば完了。

f:id:shiccocsan:20171005124956p:plain

subsonic apiでログイン

サーバにlibresonicをいれたので,自分でクライアント作ってみたいと考えている。

github.com

APIお試しのためのコードをPythonで書いた。

gist.github.com

以下のようなレスポンスが帰ってきたらOK。

<?xml version="1.0" encoding="UTF-8"?>
<subsonic-response xmlns="http://libresonic.org/restapi" status="ok" version="1.13.0"/>