2014年9月25日

JavaScriptが実行できるリアルタイムオンラインエディタ作ってみました

複数人で同期して編集できるWebエディタを探していたのですが、 見つからず・・・

jsfiddlejsdo.it は同時編集できないため、ちょっと探してたのとは違って、
Scratchpad はリアルタイムで編集できてよいのですが、JavaScriptが実行できない。
CodePen は同期するのに PRO アカウントが必要らして、諦めて、仕方ないので作ってみました。

追記:
SyncFiddle はリニューアルし、使用しているフレームワークの構成が変わりました。
URL は http://syncfiddle.net/ になりました。

仕様とか

  • アクセスするとランダムにid振ったページ空間に飛ぶ
  • そのURLを共有すると複数人で同時編集できる
  • 相手のことがわかるのはidだけ
  • コードとカーソル位置がわかる
  • JavaScript コンソールがある
  • その時のHTMLコードを .html で保存できる
  • ページの破棄という機能はないですが、コードを空にしてユーザ数 0 になるとそのうち消える
  • プライベートタブ(シークレットタブ)とか使ってみるとわかりやすいかも

レイアウト

  • PHP フレームワーク PunyApp
  • JavaScript Backbone + r.js
  • エディタは CodeMirror の HTML5 mixed モード

こないだ公開したフレームワーク PunyApp のテストも兼ねて作ってみたのですが、 なにしろサーバが古くて大変。最初は WebSocketにしようと思ったけどフレームワークのテストにならないので pollingしてます。今更ですがサーバーから立て直したほうがよかったです。一応公開してますが、サーバが不安定になるかもしれないです。
Backbone.js と r.js (require.js)、前から触ってみたくて、今回はじめて使ってみました。
自由度が高くて、View周りとかちょっと道を外れると ぐしゃってしまうので注意が必要ですが、 自分にとっては使い心地がよかった。まだまだ勉強中です。



2014年9月6日

軽量PHPフレームワークPunyAppというの作りました


最近、ちまちまとPHP を書くことがあって、 ちょっとしたWebアプリ等作るときに
CakePHP などのある程度規模のあるフレームワークを使うまでもないかなって思うことが多々あり、
そういった用途に 小規模向けなフレームワーク (マイクロフレームワーク) PunyApp というのを作りました。

以前から自分だけで使ってて少しずつ更新して温めていたもので、 ある程度形になったので公開しました。
軽量で中小規模向けなもので、 おおまかな設計は CakePHP を参考にしてて、使い方も似てます。

PunyApp は MVC モデルで他のPHP拡張等は必要ありません。

ダウンロード

要件

  • PHP 5.2.0 +
  • mod_rewrite が有効 (Apache Server)

ライセンス

  • MIT

機能/特徴

  • MySQL, PostgreSQL, SQLite, Posql に対応
  • Controller, Model, Viewアクション
  • フォームのValidation
  • Viewテンプレート変数はデフォルトでHTMLエスケープされる
  • Session (データベース)
  • Cookie (デフォルトで暗号化)
  • データベースエラー時などのEvent
  • サンプルのログインフォームが入ってる


追記

以下の情報はバージョンアップに伴い古くなっているので https://github.com/polygonplanet/PunyApp を参照ください

レイアウト

  • ファイル構成
/application                → アプリケーションディレクトリ
    /controllers            → コントローラ
    /models                 → モデル
    /views                  → ビュー
    /libraries              → 共通ライブラリやヘルパー等
    /storage                → SQLiteなどのファイルベースデータベースやログを保管
        /logs               → /storage配下を書き込み可にしておく
        /databases          → 同様
    /settings               → 設定
        app-settings.php    → アプリケーション設定ファイル
        app-scheme.php      → データベーススキーマ
    /public                 → 公開ディレクトリ
        /css
        /js
        index.php
/lib                        → PunyApp内部ライブラリ
/vendors                    → 外部ライブラリ等を入れるディレクトリ
index.php

Controllers

コントローラのアクションは、GET や POST などのリクエストメソッドで切り分けができます。

class SampleController extends PunyApp_Controller {

  public $models = array('sample');

  /**
   * GET /login
   */
  public function getLogin($params) {
    $this->view->render('sample/login');
  }

  /**
   * POST /login
   */
  public function postLogin($params) {
    $has = $this->sample->hasUser($params['id'], $params['pass']);
    if ($has) {
      $this->session->userid = $params['id'];
      $this->redirect('home');
    }

    // ...

    $this->view->render('sample/login');
  }

  /**
   * Any /login (あらゆるリクエストメソッドに対応)
   */
  public function anyLogin($params) {
    // ...
  }

  /**
   * Before /login (前処理)
   */
  public function beforeLogin($params) {
    if (!empty($this->session->userId)) {
      $this->redirect('home');
    }
  }

  /**
   * After /login (後処理)
   */
  public function afterLogin($params) {
    // ...
  }

  /**
   * GET /home
   */
  public function getHome($params) {
    // ...
  }
}
before や after で前処理などが設定できます。
any は、すべてのリクエストメソッドに対応します。
メソッド名を any だけ (function any() {}) にすると、404 にならずに any が実行されます。
引数 $params はリクエストパラメータが渡されます。

コントローラのメソッド名の命名規則は、

リクエストメソッド名 + アクション名

になります。
GET に対応する hoge だったら、getHoge になります。

URL は、
http://www.example.com/コントローラ名/アクション名

となります。
アクション名は、リクエストメソッド名を外した名前です。
SampleController で getHoge の場合は、
http://www.example.com/sample/hoge
という感じになります。 ファイル名は sample.php です

AnyController (any.php) という名前にすると、すべてのリクエストに対応します。

Models

モデルは、PDO を使っているのもあり 基本的にプリペアドステートメントを使って実行します (プレースホルダ)。

class SampleModel extends PunyApp_Model {

  public function addUser($userid, $email, $pass) {
    // Insert
    $sample = $this->newInstance();
    $sample->userid = $userid;
    $sample->email = $email;
    $sample->pass = sha1($pass);
    return $sample->save();
  }


  public function deleteUser($userid) {
    return $this->delete(
      array('userid' => '?'),
      array($userid)
    );
  }


  public function getUser($userid) {
    return $this->findOne(
      array(
        'fields' => array('id', 'userid', 'email'),
        'where' => array('userid' => '?')
      ),
      array($userid)
    );
  }


  public function hasUser($userid, $pass) {
    return $this->has(
      array(
        'where' => array(
          'userid' => ':userid',
          'pass' => ':pass'
        )
      ),
      array(
        ':userid' => $userid,
        ':pass' => sha1($pass)
      )
    );
  }
}

  • array find ( array $query = array(), array $params = array())

find() の引数 $query は、 'distinct', 'fields', 'from', 'as', 'joins', 'where', 'group', 'having', 'order', 'limit', 'offset' が使えます。
  public function getUserByName($name) {
    return $this->find(
      array(
        'distinct' => false,
        'fields' => array(
          'U.id AS id', 'U.name AS name',
          'U.category AS cat', 'P.url AS url'
        ),
        'as' => 'U',
        'joins' => array(
          'type' => 'LEFT',
          'table' => 'profile',
          'as' => 'P',
          'on' => array('U.id' => 'P.id')
        ),
        'where' => array(
          'name' => ':name'
        ),
        'group' => 'cat',
        'order' => 'id DESC',
        'limit' => 10,
        'offset' => 5
      ),
      array(
        ':name' => $name
      )
    );
  }
ある程度のクエリは扱えます。

モデル内では $this->getDatabase() が PDO として使えるので、

  public function getName($id) {
    $sql = 'SELECT name FROM foo WHERE id = ?';
    $stmt = $this->getDatabase()->prepare($sql);
    $stmt->execute(array($id));
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
  }
上のように直接クエリを書いてもいいですが、
直接クエリを書かずにモデルの find() や delete() 等を使うメリットとしては、
'created''modified' というフィールドが自動てセットされることが大きいです。 テーブル定義にあらかじめ created もしくは modified を定義しておくと、 insert (save), update のタイミングでそれぞれ現在時刻を設定します。
integer や int(11) で定義すると 現在の time() が設定されます。
datetime は Y-m-d H:i:s になります。
varchar(255) で定義すると現在時刻をミリ秒で設定します。
  • created : 作成された日時
  • modified : 更新された日時
これらはアプリケーション側で created, modified をパラメータに扱う場合は無視されます。

コントローラでのモデル定義は $models に使うモデル名(テーブル名)を記述します。

class SampleController extends PunyApp_Controller {

  public $models = array('sample');

  public function getLogin($id) {
    $user = $this->sample->getUser( ... );
  }
}

Views


$this->view->text = 'Hello!';
$this->view->render('index');
ビューでは PHP本来が持ってるテンプレートを使います。

<html>
  <body>
    <h1>Sample</h1>
    <?php echo $text; ?>
  </body>
</html>

テンプレート変数はデフォルトでHTMLエスケープされます。

  $this->view->text = '<script>alert(1)</script>';


  <p><?php echo $text ?></p> // &lt;script&gt;alert(1)&lt;/script&gt;


Validation

リクエストパラメータのバリデーションはコントローラ内で行います。

class SampleController extends PunyApp_Controller {

  public $validationRules = array(
    'id' => array(
      'required' => true,
      'rule' => array('regex', '/^[a-z0-9]{1,10}$/i'),
      'message' => 'Only letters and integers, max 10 characters'
    ),
    'email' => array(
      'required' => true,
      'rule' => array('email'),
      'message' => 'Invalid email address'
    ),
    'pass' => array(
      'required' => true,
      'rule' => array(
        array('minLength', 4),
        array('maxLength', 20)
      ),
      'message' => 'Min 4 characters, max 20 characters'
    )
  );

  // ...
}
バリデーションのルールは
email, url, ip, between, minLength, maxLength, regex
等が定義されています。または自分で定義します。

その他、コールバックが使えます。

  $this->validationRules['nickname'] = array(
    'required' => true,
    'rule' => array('callback', function ($value) {
      return preg_match('/^[ぁ-ん]/u', $value) && preg_match('/[!]$/u', $value);
    }),
    'message' => 'ひらがなではじまって「!」で終わらないとダメです'
  );
$this->validate() で実行します。 任意のルールを引数に渡すこともできます。
引数を省略すると $validationRules に対して実行します。

class SampleController extends PunyApp_Controller {

  // ...

  public function postLogin($params) {
    if ($this->validate()) {
      // ...
    }
  }
}
以下のViewメソッドからメッセージを取得できます。

追記

以下の情報はバージョンアップに伴い互換性があるかわかりません
詳細は https://github.com/polygonplanet/PunyApp を参照ください

  • 名前を指定して取得 : string getValidationError( string $name );
  • 最後のメッセージを取得 : string getLastValidationError( );
  • 全部取得 : array getValidationErrors( );
  • HTML<li>で全部取得 : string getValidationErrorMessages( array $attributes = array('style' => 'color:red') );
コントローラ内では $this->view->getValidationError('hoge') のように取得できます。
ビュー内では以下のようにして表示できます。

  <div class="error">
    <?php echo $this->getValidationError('param_name') ?>
  </div>

インストールと設定

1. 展開したファイルをサーバーの任意のディレクトリに置きます

2. application/settings/app-settings.php を開いて設定します
$settings = array(
  /**
   * System settings
   */
  'system' => array(

    /**
     * Debug mode
     *
     * true = show errors
     * false = hide errors
     */
    'debug' => true, // デバッグモードだとエラーが起きたとき表示される

    /**
     * Timezone
     *
     * e.g., 'America/Chicago', 'Asia/Tokyo' etc.
     */
    'timezone' => '', // 日本の場合はここを 'Asia/Tokyo' に設定しておく

    ...
  ),

  /**
   * Database settings
   */
  'database' => array(

    /**
     * Database engine
     *
     * Available engines: "mysql", "sqlite" and "posql".
     */
    'engine' => '', // 使用するデータベース

    ...
  ),

  /**
   * Session settings
   */
  'session' => array(

    /**
     * Session engine
     *
     * Available engines: "php", "file" and "database".
     */
    'engine' => '', // セッションエンジンを設定

    ...
  )
);
3. データベーススキーマを作成します、または application/settings/app-schema.php に記述します

4. application/storage 配下のファイル、ディレクトリのパーミッションを「書き込み可」に設定します

5. 1 で展開したディレクトリにブラウザでアクセスしてみて、PunyApp ~と表示されたら成功です

6. 実際に作ってみましょう

サンプル

/sample/ にサンプルのログインフォームが入っています。
/sample/ にアクセスして確認できます。




なにかあれば以下のレポジトリのIssues, または @polygon_planet へお願いします。

レポジトリ