2019年1月24日までにやったこと
今回は, 自分がやったことをまとめていく.
研究室のこと
1 月 21 日の月曜日に, 僕が先生に口を滑らせた.
CoAP API を作成していること. CoAP API で GET メソッドが無事動作確認が済んだこと.
データベースからデータを取り出し,ペイロードに値をセットすることが可能になった.
ただ, 値は文字化けしまくり...
さらに, POST が出来ていなかった. しかも,肝心なデータベースに値を格納するところ...
国際学会にアサインされた.
これは, 自分で悪い結果のまま終わることが出来ないので, これ以降徹夜で作業した.
1 月 22 日 火曜日
企業さんと面談を 1 時間程度した. 本当に楽しかった.
面談が終わり次第, 急いで自分で作ったバグを片付ける.
ペイロードに設定した値の文字化けが直ったので, 良かった.
あとは,POST 出来るようにすれば, 色々なことが検証できる.
1 月 23 日 水曜日
名大と進捗報告会をした.
3 年生である僕は,4 年生の先輩の発表を聞き,ずっとコーディングしてました...ごめんなさい.
16 時くらいに POST の処理がエラーなく, 処理することが出来た.
その後, 飲み会会場へ
2 次会はボーリングでとても楽しかった.
1 月 24 日 木曜日
朝起きるのが遅かった.まじでポンコツ.
Mighty Geckoを用いて, GET と POST の通信が出来るか確認した. 無事成功 🎉
実機でも動いたので, 本当に良かった.
論文を書き始める.
気付いたこと
人間追い込まれると何でも出来る.
出来る人間がどんどん進化していく.
2019年1月8日にやったこと
「よしかわ、最近、何やってんの?」ってことをたまに聞かれる。
自分の口で言うのがめんどくさいので、分かりやすく日記のように書いていきます。
今回は、家のデスクトップ PC に開発環境を構築しました。あとは、研究室の自分の研究のこと。
今まで Fedora だったんですけど、OBS が NVIDIA のドライバを上手く読み込んでくれなかったので、Windows に全て移行します。
Windows で環境構築
SSD 500GB を追加しました。
WSL(Windows Subsystem for Linux)を有効化しました。
Windows に Microsoft Store 経由で ubuntu を追加しました。
Windows と ubuntu の共有フォルダの作成に成功した。
共有フォルダの作成
まずは、Windows 上の任意の場所にフォルダを作成します。
次に ubuntu を立ち上げます。
C ドライブに作成したのであれば、/mnt/c/○○
に先程作成したフォルダが作成されています。
ubuntu で以下のコマンドを実行。
C ドライブに test フォルダを作成した例です。
ln -s /mnt/c/test ~/test
シンボリックリンクを貼れば良いってことですね。
研究室のこと
HTTP プロトコルの API は作成はある程度終わったので、CoAP プロトコルの API を作成しています。
Golang で作ってます。一応、Clean Architecture で書いてます。ぶっちゃけやらなくていい
CoAP は IoT 用のプロトコルなので、パケットヘッダが 4 バイトで、UDP 通信です。
CoAP のデバッグは curl で出来ないので、以下のリポジトリをダウンロードして実行します。
今年は、セキュアな通信にするために研究を進めていきます。
気付いたこと
OBS のハードウェアエンコードすごい。
2019年にやりたいことを書いていく
どうも、吉川です。
2019 年を振り返るためのリマインダー程度にメモっていこうと思います。
対象読者
- ぼく
キープしたいこと
- ブログなどのアウトプットの数はこのまま維持していきたい。
やりたいこと
プライベート
- 論文は余裕を持って書く
- 自炊を今まで以上に頑張る
- 毎日 1 万歩以上歩く
- 1 週間に 1 回くらいブログ更新する
技術関連
- 本を年間 50 冊読む
- 研究を進めて、さらに付加価値をつけていく
- 英語のドキュメントを読んで日本語訳したのをアウトプットする
- Golang の理解を深める
- React の理解を深める
やりたいことは幾らでもありますが、どんどん追記していくことにします。
例え出来なかったとしても、自分を卑下せず自信を持って頑張っていきます。
2018年振り返る
対象読者
- ぼく
はじめに
2018 年、とても成長出来たし、とてもつらかった。とてもたのしかった。
良い経験も悪い経験もさせてもらった。
4 月
学部 3 年生になりました。
研究室配属が GW 前に行われました。
5 月
研究室の先生の元でバイトしてました。
内容は AWS 関係です。
6 月
ずっと PHP 触ってました。
あとは、JavaScript を少々。
7 月
Golang を真剣に触り始めたのはこれくらいだった気がする。
8 月
研究室旅行で三重に行きました。
VOYAGE GROUP さんのインターンに参加しました。本当に楽しかった。
9 月
インターンが終わり、研究室のことに注力し始める。
インターンで培った技術を元にめちゃくちゃ頑張るようにしてた。
10 月
大学サークル内で勉強会を月 1 で行って技術向上したつもり。
11 月
チーム開発って何だっけ、今後どうやっていけば良いんだろうって必死に考えてた。
Team Geekとか読んでたわ。
12 月
大学サークルのサーバメンテしてました。
本当に楽しかった。
まとめ
結構この 1 年やってきたんだなぁの気持ちで終わる。
CakePHPで簡単にSchedule管理アプリを作る
どうも、よしかわです。
今回は、学校のサークルなどで PHP で Web サービスを作成したいとのことだったので綴っていきます。
コードはこちらから見ることが出来ます.
目次
対象読者
データベースの設定
マイグレーション
以下のコマンドをターミナルで実行します.
bin/cake bake migration CreateUsers username:string email:string:unique:EMAIL_INDEX password:string created modified
config/Migrations/CreateUsers.php
<?php use Migrations\AbstractMigration; class CreateUsers extends AbstractMigration { /** * Change Method. * * More information on this method is available here: * http://docs.phinx.org/en/latest/migrations.html#the-change-method * @return void */ public function change() { $table = $this->table('users'); $table->addColumn('username', 'string', [ 'default' => null, 'limit' => 255, 'null' => false, ]); $table->addColumn('email', 'string', [ 'default' => null, 'limit' => 255, 'null' => false, ]); $table->addColumn('password', 'string', [ 'default' => null, 'limit' => 255, 'null' => false, ]); $table->addColumn('created', 'datetime', [ 'default' => null, 'null' => false, ]); $table->addColumn('modified', 'datetime', [ 'default' => null, 'null' => false, ]); $table->addIndex([ 'email', ], [ 'name' => 'EMAIL_INDEX', 'unique' => true, ]); $table->create(); } }
以下のコマンドをターミナルで実行します.
bin/cake bake migration CreateScehdules user_id:int:foreign title:string description:text schedulememo:string scheduledate:date starttime:time endtime:time created modified
config/Migrations/CreateSchedules.php
<?php use Migrations\AbstractMigration; class CreateSchedules extends AbstractMigration { /** * Change Method. * * More information on this method is available here: * http://docs.phinx.org/en/latest/migrations.html#the-change-method * @return void */ public function change() { $table = $this->table('schedules'); $table->addColumn('user_id', 'string', [ 'default' => null, 'limit' => 255, 'null' => false, ]); $table->addColumn('title', 'string', [ 'default' => null, 'limit' => 255, 'null' => false, ]); $table->addColumn('description', 'text', [ 'default' => null, 'null' => false, ]); $table->addColumn('schedulememo', 'string', [ 'default' => null, 'limit' => 255, 'null' => false, ]); $table->addColumn('scheduledate', 'date', [ 'default' => null, 'null' => false, ]); $table->addColumn('starttime', 'time', [ 'default' => null, 'null' => false, ]); $table->addColumn('endtime', 'time', [ 'default' => null, 'null' => false, ]); $table->addColumn('created', 'datetime', [ 'default' => null, 'null' => false, ]); $table->addColumn('modified', 'datetime', [ 'default' => null, 'null' => false, ]); $table->addIndex([ 'user_id', ], [ 'name' => 'BY_USER_ID', 'unique' => false, ]); $table->create(); } }
Bake で自動生成する
ターミナルで以下のコマンドを実行していきます.
bin/cake bake all users bin/cake bake all schedules
多分これで,ほとんどのファイルが自動で生成されたはずです.
次にログイン機構などを作成していきます.
ログイン機構を作成
上記を参考にして,作成していきます.
src/Model/Entity/User.php を編集していきます.
<?php namespace App\Model\Entity; use Cake\ORM\Entity; use Cake\Auth\DefaultPasswordHasher; /** * User Entity * * @property int $id * @property string $username * @property string $email * @property string $password * @property \Cake\I18n\FrozenTime $created * @property \Cake\I18n\FrozenTime $modified * * @property \App\Model\Entity\Schedule[] $schedules */ class User extends Entity { /** * Fields that can be mass assigned using newEntity() or patchEntity(). * * Note that when '*' is set to true, this allows all unspecified fields to * be mass assigned. For security purposes, it is advised to set '*' to false * (or remove it), and explicitly make individual fields accessible as needed. * * @var array */ protected $_accessible = [ 'username' => true, 'email' => true, 'password' => true, 'created' => true, 'modified' => true, 'schedules' => true ]; /** * Fields that are excluded from JSON versions of the entity. * * @var array */ protected $_hidden = [ 'password' ]; // _setPassword is to set password protected function _setPassword($password) { return (new DefaultPasswordHasher)->hash($password); } }
src/Controller/UsersController.php
<?php namespace App\Controller; use App\Controller\AppController; use Cake\Utility\Security; use Cake\Event\Event; /** * Users Controller * * @property \App\Model\Table\UsersTable $Users * * @method \App\Model\Entity\User[]|\Cake\Datasource\ResultSetInterface paginate($object = null, array $settings = []) */ class UsersController extends AppController { public function beforeFilter(Event $event) { parent::beforeFilter($event); // ユーザーの登録とログアウトを許可します。 $this->Auth->allow(['add', 'logout']); } public function login() { if ($this->request->is('post')) { $user = $this->Auth->identify(); if ($user) { $this->Auth->setUser($user); return $this->redirect($this->Auth->redirectUrl()); } $this->Flash->error(__('Invalid email or password, try again')); } } public function logout() { return $this->redirect($this->Auth->logout()); } /** * Index method * * @return \Cake\Http\Response|void */ public function index() { $users = $this->paginate($this->Users); $this->set(compact('users')); } /** * View method * * @param string|null $id User id. * @return \Cake\Http\Response|void * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. */ public function view($id = null) { $user = $this->Users->get($id, [ 'contain' => ['Schedules'] ]); $this->set('user', $user); } /** * Add method * * @return \Cake\Http\Response|null Redirects on successful add, renders view otherwise. */ public function add() { $user = $this->Users->newEntity(); if ($this->request->is('post')) { $user = $this->Users->patchEntity($user, $this->request->getData()); if ($this->Users->save($user)) { $this->Flash->success(__('The user has been saved.')); return $this->redirect(['action' => 'index']); } $this->Flash->error(__('The user could not be saved. Please, try again.')); } $this->set(compact('user')); } /** * Edit method * * @param string|null $id User id. * @return \Cake\Http\Response|null Redirects on successful edit, renders view otherwise. * @throws \Cake\Network\Exception\NotFoundException When record not found. */ public function edit($id = null) { $user = $this->Users->get($id, [ 'contain' => [] ]); if ($this->request->is(['patch', 'post', 'put'])) { $user = $this->Users->patchEntity($user, $this->request->getData()); if ($this->Users->save($user)) { $this->Flash->success(__('The user has been saved.')); return $this->redirect(['action' => 'index']); } $this->Flash->error(__('The user could not be saved. Please, try again.')); } $this->set(compact('user')); } /** * Delete method * * @param string|null $id User id. * @return \Cake\Http\Response|null Redirects to index. * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. */ public function delete($id = null) { $this->request->allowMethod(['post', 'delete']); $user = $this->Users->get($id); if ($this->Users->delete($user)) { $this->Flash->success(__('The user has been deleted.')); } else { $this->Flash->error(__('The user could not be deleted. Please, try again.')); } return $this->redirect(['action' => 'index']); } }
src/Controller/AppController.php
<?php /** * CakePHP(tm) : Rapid Development Framework (https://cakephp.org) * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) * * Licensed under The MIT License * For full copyright and license information, please see the LICENSE.txt * Redistributions of files must retain the above copyright notice. * * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) * @link https://cakephp.org CakePHP(tm) Project * @since 0.2.9 * @license https://opensource.org/licenses/mit-license.php MIT License */ namespace App\Controller; use Cake\Controller\Controller; use Cake\Event\Event; /** * Application Controller * * Add your application-wide methods in the class below, your controllers * will inherit them. * * @link https://book.cakephp.org/3.0/en/controllers.html#the-app-controller */ class AppController extends Controller { /** * Initialization hook method. * * Use this method to add common initialization code like loading components. * * e.g. `$this->loadComponent('Security');` * * @return void */ public function initialize() { parent::initialize(); $this->loadComponent('RequestHandler', [ 'enableBeforeRedirect' => false, ]); $this->loadComponent('Flash'); $this->loadComponent('Auth', [ 'authenticate' => [ 'Form' => [ 'fields' => [ 'username' => 'email', 'password' => 'password' ] ] ], 'loginRedirect' => [ 'controller' => 'Schedules', 'action' => 'index' ], 'logoutRedirect' => [ 'controller' => 'Users', 'action' => 'login' ] ]); /* * Enable the following component for recommended CakePHP security settings. * see https://book.cakephp.org/3.0/en/controllers/components/security.html */ //$this->loadComponent('Security'); } }
src/Template/Users/login.ctp
<?php /** * Copyright 2010 - 2017, Cake Development Corporation (https://www.cakedc.com) * * Licensed under The MIT License * Redistributions of files must retain the above copyright notice. * * @copyright Copyright 2010 - 2017, Cake Development Corporation (https://www.cakedc.com) * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ use Cake\Core\Configure; ?> <?= $this->Html->css('users') ?> <?= $this->fetch('head') ?> <div class="login-form"> <?= $this->Flash->render('auth') ?> <?= $this->Form->create() ?> <fieldset> <legend class="form-title"><?= __d('Users', 'Login') ?></legend> <?= $this->Form->input('email', array('label' => false, 'required' => true, 'placeholder' => 'email')) ?> <?= $this->Form->input('password', array('label' => false, 'required' => true, 'placeholder' => 'password')) ?> </fieldset> <?= $this->Form->button(__d('Users', 'Login')); ?> <?= $this->Form->end() ?> </div>
ログイン画面も作成していきましょう.
src/Template/Users/login.ctp
<?php /** * Copyright 2010 - 2017, Cake Development Corporation (https://www.cakedc.com) * * Licensed under The MIT License * Redistributions of files must retain the above copyright notice. * * @copyright Copyright 2010 - 2017, Cake Development Corporation (https://www.cakedc.com) * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ use Cake\Core\Configure; ?> <?= $this->Html->css('users') ?> <?= $this->fetch('head') ?> <div class="login-form"> <?= $this->Flash->render('auth') ?> <?= $this->Form->create() ?> <fieldset> <legend class="form-title"><?= __d('Users', 'Login') ?></legend> <?= $this->Form->input('email', array('label' => false, 'required' => true, 'placeholder' => 'email')) ?> <?= $this->Form->input('password', array('label' => false, 'required' => true, 'placeholder' => 'password')) ?> </fieldset> <?= $this->Form->button(__d('Users', 'Login')); ?> <?= $this->Form->end() ?> </div>
スケジュールを見れるようにする
src/Controller/SchedulesController.php
<?php namespace App\Controller; use App\Controller\AppController; /** * Schedules Controller * * @property \App\Model\Table\SchedulesTable $Schedules * * @method \App\Model\Entity\Schedule[]|\Cake\Datasource\ResultSetInterface paginate($object = null, array $settings = []) */ class SchedulesController extends AppController { /** * Index method * * @return \Cake\Http\Response|void */ public function index() { $this->paginate = [ 'contain' => ['Users'] ]; $schedules = $this->paginate($this->Schedules); $this->set(compact('schedules')); } /** * View method * * @param string|null $id Schedule id. * @return \Cake\Http\Response|void * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. */ public function view($id = null) { $schedule = $this->Schedules->get($id, [ 'contain' => ['Users'] ]); if ($this->Auth->user('id') == $schedule['user_id']) { $this->set('schedule', $schedule); $this->set('_serialize', ['schedule']); } else { $this->Flash->error(__('ユーザーIDが違います。同じユーザーIDのみ視聴できます。')); return $this->redirect(['action' => 'index']); } } /** * Add method * * @return \Cake\Http\Response|null Redirects on successful add, renders view otherwise. */ public function add() { $schedule = $this->Schedules->newEntity(); if ($this->request->is('post')) { $schedule = $this->Schedules->patchEntity($schedule, $this->request->getData()); if ($this->Schedules->save($schedule)) { $this->Flash->success(__('The schedule has been saved.')); return $this->redirect(['action' => 'index']); } $this->Flash->error(__('The schedule could not be saved. Please, try again.')); } $users = $this->Schedules->Users->find('list', ['limit' => 200]); $this->set(compact('schedule', 'users')); } /** * Edit method * * @param string|null $id Schedule id. * @return \Cake\Http\Response|null Redirects on successful edit, renders view otherwise. * @throws \Cake\Network\Exception\NotFoundException When record not found. */ public function edit($id = null) { $schedule = $this->Schedules->get($id, [ 'contain' => [] ]); if ($this->request->is(['patch', 'post', 'put'])) { $schedule = $this->Schedules->patchEntity($schedule, $this->request->getData()); if ($this->Schedules->save($schedule)) { $this->Flash->success(__('The schedule has been saved.')); return $this->redirect(['action' => 'index']); } $this->Flash->error(__('The schedule could not be saved. Please, try again.')); } $users = $this->Schedules->Users->find('list', ['limit' => 200]); $this->set(compact('schedule', 'users')); } /** * Delete method * * @param string|null $id Schedule id. * @return \Cake\Http\Response|null Redirects to index. * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. */ public function delete($id = null) { $this->request->allowMethod(['post', 'delete']); $schedule = $this->Schedules->get($id); if ($this->Schedules->delete($schedule)) { $this->Flash->success(__('The schedule has been deleted.')); } else { $this->Flash->error(__('The schedule could not be deleted. Please, try again.')); } return $this->redirect(['action' => 'index']); } /** * Weekly method * * @return \Cake\Http\Response|void */ public function weekly() { $schedules = $this->Schedules->find()->where(["user_id = " => $this->Auth->user('id')]) ->andwhere(["DATE(scheduledate) >= CURDATE()"]) ->andwhere(["DATE(scheduledate) <= DATE(DATE_ADD(CURDATE(), INTERVAL 7 DAY))"]); $this->paginate($schedules); $this->set(compact('schedules')); } /** * Monthly method * * @return \Cake\Http\Response|void */ public function monthly() { $schedules = $this->Schedules->find()->where(["user_id = " => $this->Auth->user('id')]) ->andwhere(["DATE(scheduledate) >= CURDATE()"]) ->andwhere(["DATE(scheduledate) <= DATE(DATE_ADD(CURDATE(), INTERVAL 30 DAY))"]); $this->paginate($schedules); $this->set(compact('schedules')); } }
src/Template/Schedules/weekly.ctp
<?php /** * @var \App\View\AppView $this * @var \App\Model\Entity\Schedule[]|\Cake\Collection\CollectionInterface $schedules */ ?> <nav class="large-3 medium-4 columns" id="actions-sidebar"> <ul class="side-nav"> <li class="heading"><?= __('Actions') ?></li> <li><?= $this->Html->link(__('New Schedule'), ['action' => 'add']) ?></li> <li> <?= $this->Html->link(__('List Users'), ['controller' => 'Users', 'action' => 'index']) ?> </li> <li> <?= $this->Html->link(__('New User'), ['controller' => 'Users', 'action' => 'add']) ?> </li> </ul> </nav> <div class="schedules index large-9 medium-8 columns content"> <h3><?= __('Schedules') ?></h3> <table cellpadding="0" cellspacing="0"> <thead> <tr> <th scope="col"><?= $this->Paginator->sort('id') ?></th> <th scope="col"><?= $this->Paginator->sort('title') ?></th> <th scope="col"><?= $this->Paginator->sort('scheduledate') ?></th> <th scope="col"><?= $this->Paginator->sort('created') ?></th> <th scope="col"><?= $this->Paginator->sort('modified') ?></th> <th scope="col" class="actions"><?= __('Actions') ?></th> </tr> </thead> <tbody> <?php foreach ($schedules as $schedule): ?> <tr> <td><?= $this->Number->format($schedule->id) ?></td> <td><?= h($schedule->title) ?></td> <td><?= h($schedule->scheduledate) ?></td> <td><?= h($schedule->created) ?></td> <td><?= h($schedule->modified) ?></td> <td class="actions"> <?= $this->Html->link(__('View'), ['action' => 'view', $schedule->id]) ?> <?= $this->Html->link(__('Edit'), ['action' => 'edit', $schedule->id]) ?> <?= $this->Form->postLink(__('Delete'), ['action' => 'delete', $schedule->id], ['confirm' => __('Are you sure you want to delete # {0}?', $schedule->id)]) ?> </td> </tr> <?php endforeach; ?> </tbody> </table> <div class="paginator"> <ul class="pagination"> <?= $this->Paginator->first('<< ' . __('first')) ?> <?= $this->Paginator->prev('< ' . __('previous')) ?> <?= $this->Paginator->numbers() ?> <?= $this->Paginator->next(__('next') . ' >') ?> <?= $this->Paginator->last(__('last') . ' >>') ?> </ul> <p> <?= $this->Paginator->counter(['format' => __('Page {{page}} of {{pages}}, showing {{current}} record(s) out of {{count}} total')]) ?> </p> </div> </div>
src/Template/Schedules/monthly.ctp
<?php /** * @var \App\View\AppView $this * @var \App\Model\Entity\Schedule[]|\Cake\Collection\CollectionInterface $schedules */ ?> <nav class="large-3 medium-4 columns" id="actions-sidebar"> <ul class="side-nav"> <li class="heading"><?= __('Actions') ?></li> <li><?= $this->Html->link(__('New Schedule'), ['action' => 'add']) ?></li> <li> <?= $this->Html->link(__('List Users'), ['controller' => 'Users', 'action' => 'index']) ?> </li> <li> <?= $this->Html->link(__('New User'), ['controller' => 'Users', 'action' => 'add']) ?> </li> </ul> </nav> <div class="schedules index large-9 medium-8 columns content"> <h3><?= __('Schedules') ?></h3> <table cellpadding="0" cellspacing="0"> <thead> <tr> <th scope="col"><?= $this->Paginator->sort('id') ?></th> <th scope="col"><?= $this->Paginator->sort('title') ?></th> <th scope="col"><?= $this->Paginator->sort('scheduledate') ?></th> <th scope="col"><?= $this->Paginator->sort('created') ?></th> <th scope="col"><?= $this->Paginator->sort('modified') ?></th> <th scope="col" class="actions"><?= __('Actions') ?></th> </tr> </thead> <tbody> <?php foreach ($schedules as $schedule): ?> <tr> <td><?= $this->Number->format($schedule->id) ?></td> <td><?= h($schedule->title) ?></td> <td><?= h($schedule->scheduledate) ?></td> <td><?= h($schedule->created) ?></td> <td><?= h($schedule->modified) ?></td> <td class="actions"> <?= $this->Html->link(__('View'), ['action' => 'view', $schedule->id]) ?> <?= $this->Html->link(__('Edit'), ['action' => 'edit', $schedule->id]) ?> <?= $this->Form->postLink(__('Delete'), ['action' => 'delete', $schedule->id], ['confirm' => __('Are you sure you want to delete # {0}?', $schedule->id)]) ?> </td> </tr> <?php endforeach; ?> </tbody> </table> <div class="paginator"> <ul class="pagination"> <?= $this->Paginator->first('<< ' . __('first')) ?> <?= $this->Paginator->prev('< ' . __('previous')) ?> <?= $this->Paginator->numbers() ?> <?= $this->Paginator->next(__('next') . ' >') ?> <?= $this->Paginator->last(__('last') . ' >>') ?> </ul> <p> <?= $this->Paginator->counter(['format' => __('Page {{page}} of {{pages}}, showing {{current}} record(s) out of {{count}} total')]) ?> </p> </div> </div>
まとめ
今回は,簡単にログイン機構やスケジュール機能を作成しただけなのですが,結構コード量が多いですね...
CakePHPでTDDする
CakePHP で TDD する
どうも、最近、お布団から全然出れないよしかわです。
今回は CakePHP でテスト駆動開発(以下 TDD と称する)をした体験談をまとめていきます。
TDD をすると、どんなメリット、デメリットがあるかに言及していきます。
対象読者
- TDD を知らない人
- CakePHP を業務で触っている人
- 研究で TDD を取り入れたい人
TDD とは
テスト駆動開発(Test-Driven Development)とは、プログラム開発手法の一種で、プログラムに必要な各機能について、最初にテストを書き、そのテストが動作する必要最低限な実装をとりあえず行った後、コードを洗練させる、という短い工程を繰り返すスタイルである。(Wiki 引用)
Clean code that works. 「動作するきれいなコード」。Ron Jeffries の言葉が、TDD の目標です。
TDD はシンプルな 2 つのルールです。
- 自動化されたテストが失敗したときのみ、新しいコードを書く。
- 重複を除去する。
TDD の手順を紹介していきます。
- テストを書く(テストファースト)
- 実行して失敗させる
- テストが通る実装を書く
- テストを成功させる
- テストが通る状態のままコードをきれいにする
- 実装を完成させる
TDD の流れ
TDD のサイクルについて紹介します。
- レッド:動作しないテストを 1 つ書く。
- グリーン:そのテストを迅速に動作させる。このステップでは罪を犯してもよい。
- リファクタリング:テストを通すために発生した重複をすべて除去する。
メリット
- バグが少なくなる
- デバッグの時間が短くなる
- コードを書くことで具体化できる
- きれいなコードを作成できる
デメリット
- 実践するのに時間がかかる
- コーディング時間が伸びる
- テストするのが難しいケースがある
- テストコードの保守が必要
インストールする
CakePHP では PHPUnit を導入することができます。
Composer で簡単に導入してみましょう。
php composer.phar require --dev phpunit/phpunit:"^5.7|^6.0"
テストで使用する DB の設定をします。
config/app.php の以下の test の部分を変更します。
'test' => [ 'className' => 'Cake\Database\Connection', 'driver' => 'Cake\Database\Driver\Mysql', 'persistent' => false, 'host' => 'dbhost', 'username' => 'dbuser', 'password' => 'password', 'database' => 'test_database' ],
CakePHP でテストコードを自動生成
多くのフレームワークはテストコードを自動生成してくれます。
ここでは、CakePHP の bake コマンドで自動生成してみましょう。
bin/cake bake test Controller User
bake コマンドで「/tests/TestCase/Controller/UsersControllerTest.php」が生成されました。
テストしてみる
テストの実行は以下のコマンドを入力します。
vendor/bin/phpunit
まずは何も書かずに実行しましょう。
vendor/bin/phpunit PHPUnit 6.5.13 by Sebastian Bergmann and contributors. .IIIII....IIIIIII. 18 / 18 (100%) Time: 506 ms, Memory: 15.25MB OK, but incomplete, skipped, or risky tests! Tests: 18, Assertions: 24, Incomplete: 12.
tests/TestCase 下のテストコードが実行されます。
失敗から成功に
最初にテストコードを書きましょう。
今回満たす要件は以下の 3 つのことです。
- ユーザーについて確認するときに、セッションが確立されていないとリダイレクトする
- ユーザーの追加ができる
- セッション確立後はユーザーの編集が可能である
tests/TestCase/Controller/UsersControllerTest.php
public function testIndex() { $this->get('/users'); // Redirect from users '?redirect=/users' $this->assertRedirect('/users/login?redirect=%2Fusers'); } public function testAdd() { $this->post( '/users/add', [ 'name' => 'test_user', 'email' => 'test_email@hoge.com', 'password' => 'hogehoge' ] ); // 2xx/3xx Check response code $this->assertResponseSuccess(); } public function testEdit() { $this->session( [ 'Auth' => [ 'User' => [ 'id' => 1, 'name' => 'test_user', 'email' => 'test_email@hoge.com', 'password' => 'hogehoge', 'created_at' => new FrozenTime('2018-09-25 10:26:13'), 'updated_at' => new FrozenTime('2018-09-25 10:26:13') ] ] ] ); // OK accessing with GET method $this->get('/users/edit/1'); $this->assertResponseOK(); }
テストを通すために、ログイン機構が必要であるため、以下のようなコードになります。
src/Controller/UsersController.php
public function beforeFilter(Event $event) { parent::beforeFilter($event); $this->Auth->allow(['add', 'logout']); } public function login() { if ($this->request->is('post')) { $user = $this->Auth->identify(); if ($user) { $this->Auth->setUser($user); return $this->redirect($this->Auth->redirectUrl()); } $this->Flash->error(__('Invalid email or password, try again')); } } public function add() { $user = $this->Users->newEntity(); if ($this->request->is('post')) { $user = $this->Users->patchEntity($user, $this->request->getData()); if ($this->Users->save($user)) { $this->Flash->success(__('The user has been saved.')); return $this->redirect(['action' => 'index']); } $this->Flash->error(__('The user could not be saved. Please, try again.')); } $this->set(compact('user')); } public function edit($id = null) { $user = $this->Users->get($id, [ 'contain' => [] ]); if ($this->request->is(['patch', 'post', 'put'])) { $user = $this->Users->patchEntity($user, $this->request->getData()); if ($this->Users->save($user)) { $this->Flash->success(__('The user has been saved.')); return $this->redirect(['action' => 'index']); } $this->Flash->error(__('The user could not be saved. Please, try again.')); } $this->set(compact('user')); }
リファクタリングをすることがなかったので、本当のTDDにはなっていないですね…
レッドからグリーンにするために、コーディングしていきましょう。
その後から、きれいなコードにしていきます。
まとめ
TDD をしたくなりましたか?
最初からきれいで動作するコードを書くのは、とてもむずかしいですよね。
僕は CircleCI を用いた TDD をしています。やっぱり研究でも役に立つ TDD は良いですね。
ここまで読んでいただき、ありがとうございました!
参考文献
Go言語入門 2日目
Go 言語 入門 2 日目
どうも、よしかわです。
を見た上で、今回の記事を読み進めると理解が深まると思います。
対象読者
目次
参考文献
Golang ことはじめ - OthloBlog - オスロブログ -
文
セミコロンとその省略
Go では各々の「文」と「セミコロン」によって区切られます。
package main import ( "fmt" ) func main() { fmt.Println("Hello, World!") }
各文がセミコロンで区切られるはずが、上記のコードのどこにも「;」は見当たりません。
Go はコード上のすべてのセミコロンが省略可能になるよう文法が設計されているため、見た目の上では一切セミコロンが見当たらなくても不思議なことではないです。
元の「Hello, World!」プログラムは、コンパイラによって次のようなコードとして解釈されます。
package main; import ( "fmt" ); func main() { fmt.Println("Hello, World!"); };
各文の末尾にセミコロンが増えていることがわかります。Go のコンパイラは各行の文末を見て「文の終端」であると判断した場合に自動的にセミコロンを挿入します。結果的に、文を区切るセミコロンが「存在しているのに見えない」状態になっているわけです。
変数
Go におけるすべての変数は「型」を備えます。変数の肩は、大きく分けると「値型」「参照型」「ポインタ型」の 3 種類に分かれます。 「値型」は C や Java における値型と同様に整数や実数といった「値」そのものを格納する変数です。
「参照型」は少し特殊で、Go では「スライス」「マップ」「チャネル」という 3 つのデータ構造のいずれかを指し示す変数の型になります。
最後の「ポインタ型」は C の学習障壁として悪名高い「ポインタ」を表す変数です。
定数の定義
明示的な定義
Go の変数を定義する方法には、「明示的」な書き方と「暗黙的」な書き方の 2 種類があります。
まずは、明示的に変数を定義する書き方を見ましょう。
// int型の変数nを定義する var n int
予約語である var のあとに続けて「変数の名前(識別子)」を指定し、最後に「変数の型」を指定します。
このコードは int 型の変数 n の定義しています。このように、var を使用する倍位は、変数の名前と型の両方を明示的に指定して変数を定義する必要があります。
同じ型の変数であれば、次のように複数の変数をまとめて定義することも出来ます。
// int型の変数x, y, zを定義する var x, y, z int
次のように、var 以下の内容を()で書こうことで、異なる型の変数をまとめて定義することも可能です。変数定義を行うブロックが見やすくなるメリットがあります。
// int型の変数x, yとstring型の変数nameを定義する var ( x,y int name string )
定義した変数には演算子=を使用して値を代入できます。型が正しい限り、再代入への制限はありません。しかし、異なる型の値を代入しようとするとコンパイルエラーが発生します。
暗黙的な定義
変則的ですが次のような書き方も許されます。次のように
var a = 1 // int型の変数aに1を代入
演算子:=を使用する書き方に比べて明らかに冗長ですので、この例であれば間違いなく次の書き方のほうが適切でしょう。
a := 1
しかし、次のように複数の変数を暗黙的に定義する場合を考えてみましょう。
// varで変数定義をまとめる書き方 var ( n = 1 s = "string" b = true ) // 暗黙的な定義を並べる書き方 n := 1 s := "string" b := true
var で囲った書き方のほうが変数定義のブロックを目立たせることが出来るので、好ましい書き方であると言えます。
複数の変数を定義する場合は、可能な限り var にまとめることを意識しましょう。
型
論理値型
bool 型は論理値を表す型です。真を表す定数 true, 偽を表す定数 false いずれかの値をとります。
var b bool // bool型の変数bを定義 b = true // 変数bにtrueを代入
型推論を利用した変数定義も可能です。
b := false // bool型の変数bを定義してfalseを代入
数値型
Go には明確に定義された多数の数値型が用意されています。たとえば int64 という整数型がありますが、これは「64 ビット符号付き整数」を表します。
このように明確に定義された基本型を用意することによって、C における int 型が環境によって 32 ビット整数を表したり 64 ビット整数を表すような「実装依存」による取り扱いの難しさを軽減させています。
整数型
整数型は、符号付き整数と符号なし整数の 2 種類があります。符号なし整数の型は、正の整数と「0」のみを扱うことが出来ます。
一方、符号付き整数の型は、負の整数を含んだ正負の整数と「0」を扱うことが出来ます。
「int8」型や「uint8」型のように型名の最後に数値が付く型は、その数値でビット単位のサイズを表しています。
サイズを記述しない「int」型と「uint」型は、CPU アーキテクチャ次第で 4 バイト(32 ビット)か 8 バイト(64 ビット)のどちらかになります。
浮動小数点型
浮動小数点型は、「float32」型と「float64」型の 2 つがあります。
float32 型は Java の float 型と同じサイズです。また、float64 型は Java の double 型と同じサイズです。
複素数型
複素数型は、2 つの浮動小数点で実数と虚数を表す型であり、「complex64」型と「complex128」型があります。
complex64 型は、実数と虚数を float32 型で扱います。また、complex128 型は、実数と虚数を float64 型で扱います。
文字列型
文字列型は「string」型のみであり、文字列の値は、通常 UTF-8 のバイト列です。
文字列型の変数の領域には、メモリ上に存在する文字列の実体を格納するのではなく、代わりにその参照のみを格納します。
そのため、別の文字列型の変数に代入したとしても、文字列の実体はコピーされません。
また、Go 言語の文字列はイミュータブルです。つまり文字列を一度作成すると、そのメモリの内容を変更することが出来ません。
そのため、一部分のみ変更した文字列が必要な場合は、新しい文字列をメモリ上に作成する必要があります。
package main import "fmt" func main() { s := "hello, world!" fmt.Println(len(s)) fmt.Println(s[0], s[7]) }
go run main.go 13 104 119
真偽値型
真偽値型は「bool」型のみです。この型の値は、真または偽の 2 値のみを取り、それぞれ「true」(真)と「false」(偽)といった事前定義済み定数が用意されています。
比較演算の結果や条件式で使用する値も、真偽値型です。
CakePHP3をdockerで環境構築
CakePHP3 を docker で環境構築
どうも、よしかわです。
今回、CakePHP と Docker で開発するために役立つことを綴っていきたいと思います。
対象読者
- チームで CakePHP3 を開発する人
- ローカル環境を汚したくない人
- Docker を使って CakePHP3 のアプリを開発したい人
動作環境
- Ubuntu 18.04
- docker 18.06.1-ce
- docker-compose 1.22.0
docker を使って構築
docker がインストールされている環境を事前に用意してください。
使い方
GitHub で公開しているので、今回はこちらを使います
YoshikawaTaiki/CakePHP3 docker
git clone git@github.com:YoshikawaTaiki/virtualmachine-template.git cd virtualmachine-template/docker/cakephp make docker/start
ここまでで Nginx, MySQL8, PHP7, Composer の開発環境が整いました!便利ですね。
次に CakePHP3 のアプリケーションを作ります。
make php/bash cd .. composer create-project --prefer-dist cakephp/app app
これで CakePHP3 のアプリケーションの作成が完了しました!
次に app/config/app.php を編集します。
今回、データベースは MySQL なんですが、Docker コンテナの MySQL に接続します。
docker-compose.yml で定義したコンテナ名に接続するため、services の名前であるdb
を指定します。
251 行目からhost
, username
, password
を修正します。
'Datasources' => [ 'default' => [ 'className' => 'Cake\Database\Connection', 'driver' => 'Cake\Database\Driver\Mysql', 'persistent' => false, // this hostname is container service name 'host' => 'db', 'username' => 'root', 'password' => 'password',
Database にアクセスするために、config/app.php を修正したので、今度は docker 内の MySQL にデータベースを作成します。
password
と入力すると、my_app
が作成されます。
make migrate/init Enter password: password
以上で環境構築の終了です。めっちゃ簡単です。
確認してみましょう。以下の URL にアクセスします。
このような画面が表示されれば成功です!
ここまで長いようで短かったのではないでしょうか!
お疲れ様でした!それでは、良いハックライフを!!