WHAT'S NEW?
Loading...

CakePHP2.0+プラグイン+ライブラリでレスポンシブなHTML5サイトを構築してみよう【5/5】

CakePHP2.0+プラグイン+ライブラリでレスポンシブなHTML5サイトを構築してみよう【5/5】

■今回のお題
  1. InitializrでHTML5対応版Bootstrapをダウンロード (初回)
  2. Bootswatchで好きなテーマをダウンロード(2回目)
  3. CakePHPのプラグインBootStrapをダウンロード(3回目)
  4. CakePHPのSearchプラグインをダウンロード(3回目)
  5. CakePHPのDebugKitプラグインをダウンロード(3回目)
  6. 簡単なデプロイ(4回目)
  7. 掲示板を作ってみる(4回目)
  8. 掲示板をブラッシュアップしてみる
  9. クソして寝る
8〜9だ!
特に9は大事!!!( ゚∀゚ )



■何をどうするのか?

とりあえず今回は以下の様な改修を施してみようと思う。
  • ログイン画面をセンター揃えにしてこざっぱり
  • 一覧ページをキレイにして直接ポスト、削除を可能にする
それでは早速行ってみよう。


■レイアウトファイルを俺好みに

app/View/Layout/default.ctpがベースとなるテンプレートなんだけど、Bootstrapのデフォルト風な書式で描かれている。

これをちょっといじってみよう。
まずはJavaScriptの読み込みを先頭に持ってくる。
それからナビゲーションにあるホームなども取っ払ったりしてみる。

どこをどう変えるのかピンポイントで説明しても面倒くさいと思うので、default.ctpをまるごとここに乗っけてみるので、参考にされたし。

app/View/Layouts/default.ctp
<!doctype html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7" lang="en"> <![endif]-->
<!--[if IE 7]>    <html class="no-js lt-ie9 lt-ie8" lang="en"> <![endif]-->
<!--[if IE 8]>    <html class="no-js lt-ie9" lang="en"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en"> <!--<![endif]-->
<head>
 <meta charset="utf-8">
 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">

 <title>WORKGIFT</title>
 <meta name="description" content="">
 <meta name="author" content="">

 <meta name="viewport" content="width=device-width">

 <link rel="stylesheet" href="/css/bootstrap.min.css">
 <style>
 body {
   padding-top: 60px;
   padding-bottom: 40px;
 }
 </style>
 <link rel="stylesheet" href="/css/bootstrap-responsive.min.css">
 <link rel="stylesheet" href="/css/style.css">
 <script src="/js/libs/modernizr-2.5.3-respond-1.1.0.min.js"></script>
  <script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
  <script>window.jQuery || document.write('<script src="js/libs/jquery-1.7.2.min.js"><\/script>')</script>
  <script src="/js/libs/bootstrap/bootstrap.min.js"></script>
  <script src="/js/plugins.js"></script>
  <script src="/js/script.js"></script>
  <script>
    var _gaq=[['_setAccount','UA-XXXXX-X'],['_trackPageview']];
    (function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
    g.src=('https:'==location.protocol?'//ssl':'//www')+'.google-analytics.com/ga.js';
    s.parentNode.insertBefore(g,s)}(document,'script'));
  </script>
</head>
<body>
    <div class="navbar navbar-fixed-top">
      <div class="navbar-inner">
        <div class="container">
          <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </a>
          <a class="brand" href="#">WORKGIFT</a>
          <div class="nav-collapse">
            <ul class="nav">
              <?php if($login):?>
              <li><?php echo $this->Html->link('ログアウト', '/users/logout', null, 'ログアウトする?');?></li>
              <?php endif?>
            </ul>
          </div><!--/.nav-collapse -->
        </div>
      </div>
    </div>

    <div class="container">

<?php echo $content_for_layout; ?>

      <hr>

      <footer>
        <p>© <?php echo date('Y');?> WORKGIFT All Rights Reserved.</p>
      </footer>

    </div> <!-- /container -->

</body>
</html>

■ログイン画面をてろてろてろっぴする

とりあえずこんなふうにしてみる。


これならスマートフォンで見てもキレイに映るはずだ。
いじったのは以下のファイル。

app/View/Users/login.ctp

ただ、Bootstrapのデフォルトクラスだけではココらへんうまく出来なかったので、style.cssに独自のクラスを書き、インラインスタイルも混ぜながら、色々調整してみた。

まずは先にstyle.cssに以下のようにクラスを定義しておこう。
aタグは前回のまんまだ。

app/webroot/css/style.css
/* ===== Primary Styles ========================================================
   Author:
   ========================================================================== */

.displayBlock {
  display: block;
}
.displayInline {
  display: inline;
}
.displayInlineBlock {
  display: inline-block;
}

.positionRelative {
  position: relative;
}
.positionAbsolute {
  position: absolute;
}


a:focus {
  outline: thin dotted #333;
  outline: 5px auto -webkit-focus-ring-color;
  outline-offset: -2px;
}
a:hover,
a:active {
  outline: 0;
}

a {
  color: #690;
  text-decoration: none;
}
a:hover {
  color: #9c3;
  text-decoration: underline;
}

これで気軽にセンタリングなど行うことが出来る。
早速login.ctpを以下のように書き換えよう。
かなりいじってある。

特に注意が必要なのは、後半でJavaScriptを使っているところだ。
add-onクラスのフォームのサイズを揃えるために、DOM解析後、しかし表示前というタイミングでjQueryを使ってスタイルを変更している。

この手法はソースを見る限り『なんじゃこりゃ?』と思うかもしれないが、俺はもうそういう作り手だけの満足感かはら数年前に脱しているので、ユーザがみて違和感無ければそれを正解の一つとするという意味で、これもかなり有効な手段だと認識している。

app/View/Users/login.ctp
<div class="users form">
<?php echo $this->Session->flash('auth'); ?>
<?php echo $this->Form->create('User');?>
<fieldset class="boxCenter">
  
  <div style="text-align:center; margin:0 auto;">
  <?php echo $this->Form->input('username',array(
    'prepend' => 'Eメール',
    'label' => false,
    'class' => 'span2',
  ));?>
  <?php echo $this->Form->input('password', array(
    'prepend' => 'パスワード',
    'label' => false,
    'class' => 'span2',
  ));
  ?>
    <div>
    <?php echo $this->Form->submit('<i class="icon-off icon-white"></i> ログイン', array(
      'class' => 'btn btn-primary btn-large', 'escape' => false));?>
    </div>
  </div>
  
</fieldset>
<?php echo $this->Form->end();?>
</div>

<script type="text/javascript">
$(document).ready(function(){
  
  $('span.add-on').css({
    display: 'inline-block',
    width: '6em'
  });
  
});
</script>

これでログイン画面が格好良くなった。

■一覧ページをチョメチョメボテグリ!くわッ!!!!

さて、ログイン直後に一覧ページが表示されるんだが、ここをちょっとデザイン変更しつつ、新規投稿のaddメソッドも入れてしまおうという話。

つまり一昔前のチャットみたいな感じなんだけど、Bootstrapをつかって実現すると、そう、まるでオリジナルのTwitterみたいになるわけだ。

完成形はこんな感じ。


調子にのって、Gravatarにアイコン登録してあったら勝手にアバター画像拾ってくるという仕組みを入れてある。

app/Controller/PostsController.php
App::uses('AppController', 'Controller');
class PostsController extends AppController {

  public $name = 'Posts';
  public $uses = array('User', 'Post');

  /**
   * 投稿一覧
   *
   */
  public function index() {
    $this->paginate = array(
      'recursive' => 0,
      'order' => array('Post.modified' => 'desc'),
      'limit' => 10,
    );
    $this->set('posts', $this->paginate('Post'));
  }

  /**
   * 投稿詳細
   *
   * @param type $id
   * @throws NotFoundException
   */
  public function view($id = null) {
    if(!$id) {
      throw new NotFoundException('(;´Д`)IDが無いポ');
    }
    $this->Post->id = $id;
    $this->set('post', $this->Post->read(null, $id));
  }

  /**
   * 新規投稿
   *
   */
  public function add() {
    if ($this->request->is('post')) {
      
      $this->request->data['Post']['user_id'] = $this->Auth->user('id');
      if ($this->Post->save($this->request->data)) {
        $this->Session->setFlash('保存したYO!');
        $this->redirect(array('action' => 'index'));
      } else {
        $this->Session->setFlash('保存できぬ!');
      }
    }
  }

  /**
   * 投稿編集
   *
   * @param type $id
   * @throws NotFoundException
   */
  public function edit($id = null) {
    if(!$id) {
      throw new NotFoundException('(´・ω・`)IDが無いポ');
    }
    $this->Post->id = $id;
    if ($this->request->is('get')) {
      $this->request->data = $this->Post->read();
      
      if(!empty($this->request->data['Post']['subject'])) {
        $this->request->data['Post']['body'] = 
          $this->request->data['Post']['subject']
          ."\n\n"
          .$this->request->data['Post']['body'];
      }
      
    } else {
      
      $this->request->data['Post']['user_id'] = $this->Auth->user('id');
      if ($this->Post->save($this->request->data)) {
        $this->Session->setFlash('編集したYO!');
        $this->redirect(array('action' => 'index'));
      } else {
        $this->Session->setFlash('編集できぬ!');
      }
    }
  }

  /**
   * 投稿削除
   *
   * @param type $id
   * @throws MethodNotAllowedException
   */
  public function delete($id = null) {
    if($this->request->is('get')) {
      throw new MethodNotAllowedException('(#゚Д゚)ゴルァ!!');
    }
    if ($this->Post->delete($id)) {
      $this->Session->setFlash('投稿された内容を消した!');
      $this->redirect(array('action' => 'index'));
    }
  }

  /**
   *  subjectフィールド削除用
   *  
   */
  public function mergeBody() {
    $this->Post->recursive = -1;
    $posts = $this->Post->find('all');
    $max = count($posts);
    for($i = 0; $i < $max; $i++) {
      $posts[$i]['Post']['body'] = $posts[$i]['Post']['subject']
                                 . "\n\n"
                                 . $posts[$i]['Post']['body'];
      $posts[$i]['Post']['subject'] = '';
    }
    $this->Post->query('truncate posts');
    $this->Post->saveAll($posts);
  }
}

さて、最後の『mergeBody』メソッドはなんだろう?

実は今回、投稿するときに本文のみを使用するため、件名、つまりsubjectフィールドを使わないわけだ。
テーブルから直接subjectフィールドを削除してしまってもいいんだけど、テストとはいえせっかくポストした内容を消してしまうのも忍びない。

というわけで、subjectを本文、つまりbodyに入れてしまうというメソッドをこっそり作ってみたというわけ。

だからまず最初にブラウザで

/posts/mergeBody

を見ていただきたい。一度見れば、ポストしたデータの件名が本文に入るはずだ。
そして次にpostsテーブルからsubjectフィールドは削除してしまおう。
残しておいてもいいけど、使わないものは取っておいても無駄だ。削除だ!!!

削除したらモデルファイルもあわせて編集しておこう。

app/Model/Post.php
class Post extends AppModel {

  public $name = 'Post';
  public $belongsTo = array('User');

  public $validate = array(
    'body' => array(
      'notempty' => array(
        'rule' => array('notempty'),
        'message' => '本文を入れる!!',
      ),
    ),
  );
}

さて、実際にポストが表示される際、1行目+空行であれば、1行目をタイトルとし、2行目は無視するというロジックを組んでみよう。

これはヘルパーでやって見ることにする。

app/View/Helpers/AppHelper.php
App::uses('Helper', 'View');
class AppHelper extends Helper {
  
  /**
   * bodyの1行目を件名にしてすべてを返す
   * 
   * @param array $data
   * @return array $data 
   */
  public function setSubject($data) {

    $lines = explode("\n", $data['Post']['body']);
    $data['Post']['subject'] = null;

    if(count($lines)>=3) {
      $lines[0] = trim($lines[0]);
      $lines[1] = trim($lines[1]);

      if(mb_strlen($lines[0])<30 && empty($lines[1])) {
        $data['Post']['subject'] = $lines[0];
        unset($lines[0], $lines[1]);
        $data['Post']['body'] = join("\n", $lines);
      }
    }
    return $data;
  }
}
簡単に説明すると、本文の1行目、2行目をチェックし、1行目が30文字以内、且つ2行目が空行であれば、1行目をタイトル変数に入れ、本文は先頭の2行を削除するという流れになってる。 そんなに難しくはないはずだ。 

そして次はビュー。つまりindex.ctpだ。

app/View/Posts/index.ctp
<div class="clearfix">
<div style="margin:0 auto; text-align:center;">
  <?php echo $this->Form->create('Post', array('url' => '/posts/add'));?>
  <?php echo $this->Form->input('body', array(
      'type' => 'textarea', 'label' => false, 'class' => 'span6', 'style' => 'height:8em;'))?>
  <?php echo $this->Form->submit('<i class="icon-pencil icon-white"></i> 投 稿', array(
    'escape' => false ,'class' => 'btn btn-primary btn-large', 'id' => 'submit'));?>
  <?php echo $this->Form->end();?>
</div>
</div>

<?php echo $this->Paginator->pagination(); ?>

<?php foreach ($posts as $post): ?>
<?php $post = $this->Html->setSubject($post);?>
<div class="well positionRelative" style="min-height:64px;">

  <?php if($my['id']==$post['Post']['user_id']):?>
  <div class="positionAbsolute" style="top: 1em; right: 1em">
   <?php echo $this->Html->link('<i class="icon-edit"></i>', '/posts/edit/'.$post['Post']['id'], array(
     'escape' => false, 'class' => 'btn'));?>
   <?php echo $this->Form->postLink('<i class="icon-remove-sign"></i>',
      array('action' => 'delete', $post['Post']['id']),
      array('confirm' => '削除するよ!', 'class' => 'btn', 'escape' => false));?>
  </div>
  <?php endif?>
    
  <div style="float:left;">
    <?php echo $this->Html->image('http://www.gravatar.com/avatar/'.md5(strtolower(trim($post['User']['username']))).'?s=64',
      array('alt' => $post['User']['nickname']));?>
  </div>
  <div style="margin-left:80px;">
    <h3><?php echo $post['Post']['subject'];?></h3>
    <div class="row">
      <div class="span6"><i class="icon-user"></i> <?php echo $post['User']['nickname'];?></div>
      <div class="span6"><i class="icon-time"></i> <?php echo $post['Post']['created']; ?></div>
    </div>
    <p style="margin:0.5em;"><?php echo nl2br($post['Post']['body']);?></p>
   </div>
</div>
<?php endforeach; ?>

<?php echo $this->Paginator->pagination(); ?>
さぁ、かなり変更されてるから注意が必要だ。

まずは新規ポスト用のフォーム。これはadd.ctpからコピペしたものを編集した程度。
その次に中身だけど、まずtableタグで表示するのはすべてやめ、divで可愛く仕上げてみた。

左右の配置にfloatスタイルを使っているのはCSS3的じゃないけど、気になるようであれば各々方が各々方のやり方で直すと良い。

そしてログイン時のメールアドレスがGravatarに登録されていれば、自動的にアバター画像を拾ってきて64pxで表示するという仕組みだ。

Gravatarというのは、Wordpressのコメントでも使われているけど、サービスごとにプロフィールやアバター画像を設定するのではなく、アバター画像専用のサービスに画像を登録して置けば、そのサービスに対応したものであれば勝手に登録したアバター画像が表示されるというもの。

http://ja.gravatar.com/

一昔前に流行ったプロフサイトみたいな感じだ。
1箇所で管理できるのでかなり便利なんだけど、Gravatar自体がそれほど普及していないという残念な状況になっている。俺は個人的に気に入ってるのでこれを使ってみた。

ちなみに常にGravatarから画像を引っ張ってくる設定になっている場合、アバター画像が無いときはこれも自動的にGravatarのNO-IMAGE画像が表示される。


このNO-IMAGE画像はURLのリクエストパラメータにいろいろ付け足すことで、自サーバ内の画像を指定することなんかも出来るので、なかなか便利だ。 詳しくはGravatarの開発者向け資料を読んでいただきたい。

というわけで、この一覧によって
  • add
  • view
が必要なくなったわけだ。 本当は編集であるeditも一覧ページでやってしまいたいんだけど、Ajaxなども絡んでくるので、とりあえず今回はやめることにした。

つまりeditアクションは健在なわけで、subjectフィールドを削除したりしてるから、こちらも編集して置かないといけない。


app/View/Posts/edit.ctp
<div class="btn-toolbar">
  <div class="btn-group">
    <a class="btn" href="/"><i class="icon-chevron-left"></i> 戻る</a>
  </div>
</div>

<div class="clearfix">
<div style="margin:0 auto; text-align:center;">
  <?php echo $this->Form->create('Post');?>
    <?php echo $this->Form->input('body', array(
        'type' => 'textarea', 'label' => false, 'class' => 'span6', 'style' => 'height:8em;'))?>
    <?php echo $this->Form->hidden('user_id', array('value' => $this->request->data['Post']['user_id']));?>
    <?php echo $this->Form->submit('<i class="icon-edit icon-white"></i> 編集', array(
      'class' => 'btn btn-primary btn-large', 'id' => 'submit', 'escape' => false));?>
  <?php echo $this->Form->end();?>
</div>
</div>

<script type="text/javascript">
$(document).ready(function(){

  $('span.error-message').removeClass('help-inline error-message')
     .addClass('label label-important');

});
</script>

さぁなんだか色々書いてあるね。特に後半。またJavaScriptだよ。
これは実はバリデータの操作。CakePHPが吐き出すバリデーションエラーのメッセージと、Bootstrapのスタイル、そして俺の書いたコードがそれぞれうまくマッチしないため、少々無理矢だがDOMをいじって解決させてみたという話。

もっとちゃんと調べれば必要のない処理かもしれないけど、あんまり詳しく説明するのも趣旨から外れてしまうため、手抜きの理由とさせてもらう。


■ズギャ!!

さて、これで長きに渡った連載も終了となるわけで、最後までお付き合いいただきありがとうござる。

Bootstrapの出現で一気にHTML5、CSS3、そしてレスポンシブというスタイルが普及したんだと思う。

HTML5の時代になって、覚えることが山ほど増えた。
早く覚えないと!はやく波に乗らないと!と焦る気持ちもあるのかもしれない。
けどもう大丈夫だ。ここまできたお主はもう、はっぱ隊ばりにYATTA!と言える。

とはいえ今回の連載はあくまで初歩なので、これを機に一気にHTML5、CSS3、そしてjQueryなどのテクノロジーを体と頭に通過させておこうじゃないか。

というわけでクソして寝るのでこれで終わる。