Flash helper


my_layout.ctp

<body>
    <?= $this->element('my_header'); ?>
    <?= $this->Flash->render();?>
    <div class="container">
        <?= $this->fetch('content') ?>
    </div>
</body>

Posts.Controller.php

  public function add()
  {
    $post = $this->Posts->newEntity();
    if ($this->request->is('post')) {
      $post = $this->Posts->patchEntity($post, $this->request->data);
      if($this->Posts->save($post)){
        $this->Flash->success('Add Success!');
        return $this->redirect(['action'=>'index']);
      } else {
        $this->Flash->error('Add Error!');
      }
    }
    $this->set(compact('post'));
  }
}

veiw ctpの書き方


コントローラーにメソッドを追加

<?php

// /posts/index
// /posts
// /(controller)/(action)/(options)

namespace App\Controller;

class PostsController extends AppController
{
  public function index()
  {
    $posts = $this->Posts->find('all');
      // ->order(['title' => 'DESC'])
      // ->limit(2)
      // ->where(['title like' => '%3']);
    $this->set('posts', $posts);
  }

  public function view($id = null)
  {
    $post = $this->Posts->get($id);
    $this->set(compact('post'));
  }
}

src->template->view.ctp

<?php
$this->assign('title', 'Blog Detail');
?>

<h1>
  <?= $this->Html->link('Back', ['action'=>'index'], ['class'=>['pull-right', 'fs12']]); ?>
  <?= h($post->title); ?>
</h1>

<p><?= nl2br(h($post->body)); ?></p>

Postの追加: NewEntity

<h1>
  <?= $this->Html->link('Add New', ['action'=>'add'], ['class'=>['pull-right', 'fs12']]); ?>
  Blog Posts
</h1>
  public function add()
  {
    $post = $this->Posts->newEntity();
    $this->set(compact('post'));
  }

Cakeのformヘルパー: src->template->view.ctp

<?= this->Form->create($post); ?>
<?= this->Form->input('title'); ?>
<?= this->Form->input('body', ['row'=>'3']); ?>
<?= this->Form->button('Add'); ?>

<?= this->Form->end(); ?>

データの入力

if ($this->request->is('post')){
      $post = $this->Posts->patchEntity($post, $this->request->data);
      $this->Posts->save($post);
      return $this->redirect(['action'=>'index']);
    }

validation:PostsTable.php

<?php

 namespace App\Model\Table;

 use Cake\ORM\Table;
 use Cake\Validation\Validator;

 class PostsTable extends Table
 {
   public function initialize(array $config)
   {
     $this->addBehavior('Timestamp');
   }
   public function validationDefault(Validator $validator)
   {
     $validator
       ->notEmpty('title')
       ->requirePresence('title')
       ->notEmpty('body')
       ->requirePresence('body')
       ->add('body', [
         'length' => [
           'rule' => ['minLength', 10],
           'message' => 'body length must be 10+'
         ]
       ]);
     return $validator;
   }
 }

PostsController

  public function add()
  {
    $post = $this->Posts->newEntity();
    if ($this->request->is('post')) {
      $post = $this->Posts->patchEntity($post, $this->request->data);
      if($this->Posts->save($post)){
        return $this->redirect(['action'=>'index']);
      } else {
      }
    }
    $this->set(compact('post'));
  }
}

Cakeコマンド

MVCを一気に作成するbakeコマンド

[vagrant@localhost myapp]$ bin/cake bake all posts
[vagrant@localhost myapp]$ bin/cake server -H 192.168.33.10 -p 8000

データベースの条件抽出

class PostsController extends AppController
{
  public function index()
  {
    $posts = $this->Posts->find('all')
      ->order(['title' => 'DESC'])
      ->limit(2)
      ->where(['title like' => '%3']);
    $this->set('posts', $posts);
  }
}

デフォルトレイアウト
src->Template->Layout->default.ctp
レイアウトのカスタマイズ

<!DOCTYPE html>
<html lang="ja">
<head>
    <?= $this->Html->charset() ?>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>
        <?= $cakeDescription ?>:
        <?= $this->fetch('title') ?>
    </title>
    <?= $this->Html->css('styles.css') ?>
</head>
<body>
    <div class="container">
        <?= $this->fetch('content') ?>
    </div>
</body>
</html>

PostsController.phpの編集

class PostsController extends AppController
{
  public function index()
  {
    $this->viewBuilder()->layout('my_layout');
    $posts = $this->Posts->find('all');
      // ->order(['title' => 'DESC'])
      // ->limit(2)
      // ->where(['title like' => '%3']);
    $this->set('posts', $posts);
  }
}

cssファイルはwebroot/cssにあります。

titleはindex.ctpに書くことが推奨されています。

<?php
$this->assign('title', 'blog Posts');
?>

パーツ
src->header->elementに書き込み、layoutに追記

<header>My Blog</header>
<body>
    <?= $this->element('my_header'); ?>
    <div class="container">
        <?= $this->fetch('content') ?>
    </div>
</body>

PostsController.phpはclass PostsController extends AppControllerと、AppControllerを継承しているので、AppControllerのpublic function initialize()に$this->viewBuilder()->layout(‘my_layout’);を書き込むこともあります。

リンクの張り方
HTMLヘルパー

<li><?= $this->Html->link($post->title, ['action'=>'view',$post->id]); ?></li>

URLヘルパー

<a href="<?= $this->Url->build(['action'=>'view', $post-id>]); ?>">
  <?= h(post->title); ?>
</a>

Cake.phpのインストール

Cakeは人気のあるphpのwebアプリケーションフレームワークです。centOSへのインストールは、まずcomposerをディレクトリに入れます。

curl -sS https://getcomposer.org/installer | php

次に、composerを使って、cakeをインストールしていきます。
公式サイトを参考にすると、エラーなくいけると思います。
http://book.cakephp.org/3.0/en/installation.html

[vagrant@localhost cake]$ php composer.phar create-project --prefer-dist cakephp/app myapp

%e7%84%a1%e9%a1%8c

myappに移動し、コマンドラインからサーバーを立ち上げます。

[vagrant@localhost myapp]$ bin/cake server -H 192.168.33.10 -p 8000

以下のように表示されれば、OKです。
%e7%84%a1%e9%a1%8c

完了したら、configファイルのapp.phpよりDatasourcesを探し、データベースの接続を設定していきます。東京のtimezoneは’timezone’ => ‘+09:00’,です。

加えて、bootstrap.phpでもタイムゾーンを変更します。

date_default_timezone_set('Asiz/Tokyo');

ログイン機能の実装

下のキャプチャのように、controller, model, viewにファイルを分けて作成しています。サインインデータはMysqlに格納して、ログイン時に呼び出して、判別しています。また、PostとGetでトークンの正誤判定も行っています。
%e7%84%a1%e9%a1%8c

controller.php

<?php

namespace MyApp;

class Controller {

  private $_errors;
  private $_values;

  public function __construct(){
    if(!isset($_SESSION&#91;'token'&#93;)){
      $_SESSION&#91;'token'&#93; = bin2hex(openssl_random_pseudo_bytes(16));
    }
    $this->_errors = new \stdClass();
    $this->_values = new \stdClass();
  }

  protected function setValues($key, $value){
    $this->_values->$key = $value;
  }

  public function getValues(){
    return $this->_values;
  }


  protected function setErrors($key, $error){
    $this->_errors->$key = $error;
  }

  public function getErrors($key){
    return isset($this->_errors->$key) ?  $this->_errors->$key : '';
  }

  protected function hasError(){
    return !empty(get_object_vars($this->_errors));
  }

  protected function isLoggedIn() {
    // $_SESSION['me']
    return isset($_SESSION['me']) && !empty($_SESSION['me']);
  }

  public function me(){
    return $this->isLoggedIn() ? $_SESSION['me'] : null;
  }

}

facebook SDK with composer

composerを使ったfacebook sdk
https://developers.facebook.com/docs/php/gettingstarted

[vagrant@localhost facebook]$ curl -sS https://getcomposer.org/installer | php
All settings correct for using Composer
Downloading 1.2.2...

Composer successfully installed to: /home/vagrant/facebook/composer.phar
Use it: php composer.phar

続いて、composer.jsonを作成

{
  "require" : {
    "facebook/php-sdk-v4" : "~5.0"
  }
}

その後、php composer.phar installで完了です。

jsonにautoloadを追加

{
  "require" : {
    "facebook/php-sdk-v4" : "~5.0"
  },
  "autoload":{
    "psr-4": {
      "MyApp\\": "lib/"
    }
  }
}

コマンドライン

[vagrant@localhost facebook]$ php composer.phar dump-autoload

twitter bot

まう、開発環境にcomposerをインストールします。

curl -sS https://getcomposer.org/installer | php

続いてtwitteroauthを入れます。

[vagrant@localhost bot]$ php composer.phar require abraham/twitteroauth

twitteroauthのHPに沿って、PHPを書いていきます。なお、 CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRETはtwitter developersのmy appから取得する必要があります。
https://twitteroauth.com/

<?php

require_once(__DIR__ . '/config.php');

// package
// -Composer

use Abraham\TwitterOAuth\TwitterOAuth;

$connection = new TwitterOAuth(
  CONSUMER_KEY,
  CONSUMER_SECRET,
  ACCESS_TOKEN,
  ACCESS_TOKEN_SECRET);
// $content = $connection->get("account/verify_credentials");
$content = $connection->get("statuses/home_timeline", ['count'=>3]);

var_dump($content);

crontabで指定した日時につぶやきをセットすることができます。

投票システム (1日1回)

jqueryのclick functionでopacityを変更し、更にformのvalueにidを入れて、ボタンが押されたら、MySQLにIDを格納します。そして、Mysqlからsql文でデータを読み込んで、集計結果を表示させます。また、IPアドレス、ユーザーエージェント、データで、複数投票を制限しています。

-reference
bindValue:Binds a value to a corresponding named or question mark placeholder in the SQL statement that was used to prepare the statement.
SQL INSERT INTO Statement:INSERT INTO table_name VALUES (value1,value2,value3,…);
SQLデータ取得:SELECT col_name1, col_name2, … FROM db_name.tbl_name;
SQL GROUP BY Statement:The GROUP BY statement is used in conjunction with the aggregate functions to group the result-set by one or more columns.
user agent:is software (a software agent) that is acting on behalf of a user.

index.php

<?php

require_once(__DIR__ . '/config.php');
require_once(__DIR__ . '/Poll.php');

try {
  $poll = new \MyApp\Poll();
} catch (Exception $e){
  echo $e->getMessage();
  exit;
}

if ($_SERVER['REQUEST_METHOD'] === 'POST'){
  $poll->post();
}

$err = $poll->getError();

?>
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Poll</title>
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<link rel="stylesheet" href="styles.css">
</head>
<body>
  <?php if (isset($err)) : ?>
  <div class="error"><?= h($err); ?></div>
<?php endif; ?>
  <h1>Which do you like best?</h1>
  <form action="" method="post">
    <div class="row">
      <div class="box" id="box_0" data-id="0"></div>
      <div class="box" id="box_1" data-id="1"></div>
      <div class="box" id="box_2" data-id="2"></div>
      <input type="hidden" id="answer" name="answer" value="">
      <input type="hidden" name="token" value="<?= h($_SESSION&#91;'token'&#93;); ?>">
    </div>
    <div id="btn">Vote and See Results</div>
  </form>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
	<script>
  $(function(){
    'use strict';

    $('.box').on('click', function(){
      $('.box').removeClass('selected');
      $(this).addClass('selected');
      $('#answer').val($(this).data('id'));
    });

    $('#btn').on('click', function(){
      if ($('#answer').val() === ''){
        alert('Choose one!');
      } else {
        $('form').submit();
      }
    });
    $('.error').fadeOut(3000);
  });
  </script>
</body>
</html>

Poll.php

<?php

namespace MyApp;

class Poll{
  private $_db;

  public function __construct(){
    $this->_connectDB();
    $this->_createToken();
  }

  private function _createToken(){
    if(!isset($_SESSION['token'])){
      $_SESSION['token'] = bin2hex(openssl_random_pseudo_bytes(16));
    }
  }

  private function _validateToken(){
    if(
      !isset($_SESSION['token']) ||
      !isset($_POST['token']) ||
      $_SESSION['token'] !== $_POST['token']
    ){
      throw new \Exception('invalid token!');
    }
  }

  public function post(){
    try {
      $this->_validateToken();
      $this->_validateAnswer();
      $this->_save();
      //redirect to result.php
      header('Location: http://' . $_SERVER['HTTP_HOST'] . '/result.php');
    } catch(\Exception $e){
      // set error
      $_SESSION['err'] = $e->getMessage();
      // ridirect to index.php
      header('Location: http://' . $_SERVER['HTTP_HOST']);
    }
    exit;
  }

  public function getResults(){
    $data = array_fill(0, 3, 0);

    $sql = "select answer, count(id) as c from answers group by answer";
    foreach ($this->_db->query($sql) as $row){
      $data[$row['answer']] = (int)$row['c'];
    }
    return $data;
  }

  public function getError(){
    $err = null;
    if (isset($_SESSION['err'])){
      $err = $_SESSION['err'];
      unset($_SESSION['err']);
    }
    return $err;
  }

  private function _validateAnswer(){
    // var_dump($_POST);
    // exit;
    if (
      !isset($_POST['answer']) ||
      !in_array($_POST['answer'], [0, 1, 2])
    ){
      throw new \Exception('invalid answer!');
    }
  }

  private function _save(){
    $sql = "insert into answers
          (answer, created, remote_addr, user_agent, answer_date)
          values (:answer, now(), :remote_addr, :user_agent, now())";
    $stmt = $this->_db->prepare($sql);
    $stmt->bindValue(':answer', (int)$_POST['answer'], \PDO::PARAM_INT);
    $stmt->bindValue(':remote_addr', $_SERVER['REMOTE_ADDR'], \PDO::PARAM_STR);
    $stmt->bindValue(':user_agent', $_SERVER['HTTP_USER_AGENT'], \PDO::PARAM_STR);

    try {
      $stmt->execute();
    } catch(\PDOException $e){
      throw new \Exception('No more vote for today!');
    }
    // exit;
  }

  private function _connectDB(){
    try {
      $this->_db = new \PDO(DSN, DB_USERNAME, DB_PASSWORD);
      $this->_db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
    } catch(\PDOException $e){
      throw new \Exception('Failed to connect DB');
    }
  }

}

result.php

<?php

require_once(__DIR__ . '/config.php');
require_once(__DIR__ . '/Poll.php');

try {
  $poll = new \MyApp\Poll();
} catch (Exception $e){
  echo $e->getMessage();
  exit;
}

$results = $poll->getResults();
// var_dump($results);
// exit;
// $results = [
//   0 => 12,
//   1 => 32,
//   2 => 44
// ];

?>
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Poll Result</title>
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<link rel="stylesheet" href="styles.css">
</head>
<body>
  <h1>Result ...</h1>
    <div class="row">
      <?php for($i = 0; $i < 3; $i++) : ?>
      <div class="box" id="box_<?= h($i); ?>"><?= h($results&#91;$i&#93;); ?></div>
    <?php endfor; ?>
    </div>
    <a href="/"><div id="btn">Go Back</div></a>
</body>
</html>

styles.css

body {
  font-size: 16px;
  font-family: Arial, sans-serif;
  text-align: center;
  margin: 0;
  padding: 0;
}

h1 {
  font-size: 22px;
  margin: 30px 0;
}

.row {
  margin-bottom: 15px;
}

.box {
  width: 150px;
  height: 150px;
  cursor: pointer;
  display: inline-block;
  opacity: 0.5;
  color: #fff;
  font-size: 48px;
  font-weight: bold;
  line-height: 150px;
}
.box + .box {
  margin-left: 10px;
}

#box_0 { background: url('photo_0.jpg'); }
#box_1 { background: url('photo_1.jpg'); }
#box_2 { background: url('photo_2.jpg'); }

.selected {
  opacity: 1.0;
}

#btn {
  display: inline-block;
  width: 150px;
  padding: 7px;
  font-size: 14px;
  cursor: pointer;
  border-radius: 5px;
  background: #00aaff;
  box-shadow: 0 4px 0 #0088cc;
  color: #fff;
}

#btn:hover{
  opacity: 0.8;
}

.error{
  background: orange;
  padding: 7px;
  color: #fff;
}

mySQL

create database poll_php;
grant all on poll_php.* to dbuser@localhost identified by 'xxxx';
use poll_php
create table answers (
	id int not null auto_increment primary key,
	answer int not null,
	created datetime,
	remote_addr varchar(15),
	user_agent varchar(255),
	answer_date date,
	unique unique_answer(remote_addr, user_agent, answer_date)
);

クイズアプリ

予め正しい答えを全て配列の0に入れておき、click functionで正しいか正誤判定しています。選択肢はシャッフルして表示し、最後に、正解率を表示させています。

reference
Autoloading Classes:The spl_autoload_register() function registers any number of autoloaders, enabling for classes and interfaces to be automatically loaded if they are currently not defined. By registering autoloaders, PHP is given a last chance to load the class or interface before it fails with an error.

index.php

<?php

require_once(__DIR__ . '/config.php');

$quiz = new MyApp\Quiz();

if(!$quiz->isFinished()) {
  $data = $quiz->getCurrentQuiz();
  shuffle($data['a']);
}

?>
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Intractive Art</title>
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<link rel="stylesheet" href="styles.css">
</head>
<body>
  <?php if ($quiz->isFinished()) : ?>
    <div id="container">
      <div id="result">
        your score ...
        <div><?= h($quiz->getScore()); ?>%</div>
      </div>
     <a href=""><div id="btn">Replay</div></a>
    </div>
  <?php $quiz->reset(); ?>
  <?php else : ?>
    <div id="container">
      <h1>Q. <?= h($data&#91;'q'&#93;); ?></h1>
      <ul>
        <?php foreach ($data&#91;'a'&#93; as $a) : ?>
        <li class="answer"><?= h($a); ?></li>
        <!-- <li class="answer">A1</li>
        <li class="answer">A2</li>
        <li class="answer">A3</li> -->
        <?php endforeach; ?>
      </ul>
        <div id="btn" class="disabled"><?= $quiz->isLast() ? 'Show Result' : 'Next Question'; ?></div>
        <input type="hidden" id="token" value="<?= h($_SESSION&#91;'token'&#93;); ?>">
      </div>
  	<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
    <script src="quiz.js"></script>
  <?php endif; ?>
</body>
</html>

autoload.php

<?php

spl_autoload_register(function($class){
  $prefix = 'MyApp\\';

  if (strpos($class, $prefix) === 0){
    $className = substr($class, strlen($prefix));
    $classFilePath = __DIR__ . '/' . $className . '.php';

    if (file_exists($classFilePath)){
      require $classFilePath;
    } else {
        echo 'No such class' . $className;
        exit;
    }
  }

});
&#91;/php&#93;

_answer.php
&#91;php&#93;
<?php

require_once(__DIR__ . '/config.php');

$quiz = new MyApp\Quiz();

try{
  $correctAnswer = $quiz->checkAnswer();
} catch (Exception $e) {
  header($_SERVER['SERVER_PROTOCOL']. ' 403 Forbidden', true, 403);
  echo $e->getMessage();
  exit;
}

header('Content-Type: application/json; charset=UTF-8');
echo json_encode([
  'correct_answer' => $correctAnswer
]);

Token.php

<?php

require_once(__DIR__ . '/config.php');

$quiz = new MyApp\Quiz();

try{
  $correctAnswer = $quiz->checkAnswer();
} catch (Exception $e) {
  header($_SERVER['SERVER_PROTOCOL']. ' 403 Forbidden', true, 403);
  echo $e->getMessage();
  exit;
}

header('Content-Type: application/json; charset=UTF-8');
echo json_encode([
  'correct_answer' => $correctAnswer
]);

Quiz.php

<?php

 namespace MyApp;

 class Quiz{
   private $_quizSet = &#91;&#93;;

   public function __construct(){
     $this->_setup();
     Token::create();

     if(!isset($_SESSION['current_num'])){
       $this->_initSession();
     }
   }

   public function checkAnswer(){
     Token::validate('token');
     $correctAnswer = $this->_quizSet[$_SESSION['current_num']]['a'][0];
     if (!isset($_POST['answer'])){
       throw new \Exception('answer not set!');
     }
     if($correctAnswer === $_POST['answer']){
       $_SESSION['correct_count']++;
     }
     $_SESSION['current_num']++;
     return $correctAnswer;
   }

   public function isFinished(){
     return count($this->_quizSet) === $_SESSION['current_num'];
   }

   public function getScore() {
     return round($_SESSION['correct_count'] / count($this->_quizSet) * 100);
   }

   public function isLast(){
     return count($this->_quizSet) === $_SESSION['current_num'] + 1;
   }

   public function reset(){
     $this->_initSession();
    //  $_SESSION['current_num'] = 0;
    //  $_SESSION['correct_count'] = 0;
   }

   private function _initSession(){
     $_SESSION['current_num'] = 0;
     $_SESSION['correct_count'] = 0;
   }

   public function getCurrentQuiz() {
     return $this->_quizSet[$_SESSION['current_num']];
   }

   private function _setup(){
     $this->_quizSet[] = [
       'q' => '1543年は、ある物が日本に伝えられた年でもある。それは何か?',
       'a' => ['鉄砲', 'キリスト教', '貿易', '忍法']
     ];
     $this->_quizSet[] = [
       'q' => '海上交通の要地に作られた港町で有名なのはどこ?',
       'a' => ['堺', '伊丹', '京都', '江戸']
     ];
     $this->_quizSet[] = [
       'q' => '1639年に鎖国は完成する。この年から、来航を禁止された国はどこ?',
       'a' => ['ポルトガル', 'イギリス', '清', 'オランダ']
     ];
   }
 }

config.php

quiz.js

$(function() {
  'use strict';

  $('.answer').on('click', function() {
    var $selected = $(this);
    if ($selected.hasClass('correct') || $selected.hasClass('wrong')) {
      return;
    }
    $selected.addClass('selected');
    var answer = $selected.text();

    $.post('/_answer.php', {
      answer: answer,
      token: $('#token').val()
    }).done(function(res) {
      $('.answer').each(function() {
        if ($(this).text() === res.correct_answer) {
          $(this).addClass('correct');
        } else {
          $(this).addClass('wrong');
        }
      });
      // alert(res.correct_answer);
      if (answer === res.correct_answer) {
        // correct!
        $selected.text(answer + ' ... CORRECT!');
      } else {
        // wrong!
        $selected.text(answer + ' ... WRONG!');
      }
      $('#btn').removeClass('disabled');
    });
  });

  $('#btn').on('click', function() {
    if (!$(this).hasClass('disabled')) {
      location.reload();
    }
  });

});
body {
  font-size: 16px;
  font-family: Arial, sans-serif;
}
#container {
  width: 500px;
  margin: 15px auto;
}
h1, ul > li {
  border: 1px solid #ddd;
  border-radius: 5px;
}
h1 {
  padding: 10px;
  height: 50px;
  font-size: 18px;
  margin: 0 0 10px;
}
ul {
  list-style: none;
  margin: 0 0 10px;
  padding: 0;
}
ul > li {
  margin-bottom: 7px;
  padding: 7px 10px;
  cursor: pointer;
}
#btn {
  text-align: center;
  float: right;
  width: 100px;
  padding: 7px;
  color: #fff;
  border-radius: 5px;
  background: #00aaff;
  box-shadow: 0 4px 0 #0088cc;
  cursor: pointer;
  font-size: 14px;
}
#btn.disabled {
  opacity: 0.5;
}
.correct{
  color: limegreen;
  font-weight: bold;
}
.wrong {
  color: #ddd;
}
.selected {
  font-weight: bold;
}
#result {
  border: 1px solid #ddd;
  border-radius: 5px;
  margin-bottom: 15px;
  text-align: center;
  padding: 30px 0 50px;
}
#result > div {
  font-size: 64px;
  font-weight: bold;
  margin-top: 30px;
}

todo app

mysqlと連携したtodoアプリです。削除に際して、チェックボックスはajaxを使っています。

reference
openssl_random_pseudo_bytes:Generate a pseudo-random string of bytes

<?php

session_start();

require_once(__DIR__ . '/config.php');
require_once(__DIR__ . '/functions.php');
require_once(__DIR__ . '/Todo.php');

// get todos
$todoApp = new \MyApp\Todo();
$todos = $todoApp->getAll();

//var_dump($todos);
//exit;

// create table todos(
//   id int not null auto_increment primary key,
//   state tinyint(1) default 0,
//   title text
// );
//
// insert into todos (state, title) values
// (0, 'todo 0'),
// (0, 'todo 1'),
// (1, 'todo 2');

?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>Todo app</title>
  <link rel="stylesheet" href="styles.css">
  <style>
  </style>
</head>
<body>
  <div id="container">
      <h1>Todos</h1>
      <form action="" id="new_todo_form">
        <input type="text" id="new_todo" placeholder="What needs to be done?">
      </form>
      <ul id="todos">
        <?php foreach ($todos as $todo) : ?>
          <li id="todo_<?= h($todo->id); ?>" data-id="<?= h($todo->id); ?>">
            <input type="checkbox" class="update_todo" <?php if ($todo->state === '1') { echo 'checked'; } ?>>
            <span class="todo_title <?php if ($todo->state === '1') { echo 'done'; } ?>"><?= h($todo->title); ?></span>
            <div class="delete_todo">x</div>
          </li>
        <?php endforeach; ?>
        <li id="todo_template" data-id="">
          <input type="checkbox" class="update_todo">
          <span class="todo_title"></span>
          <div class="delete_todo">x</div>
        </li>
      </ul>
  </div>
  <input type="hidden" id="token" value="<?= h($_SESSION&#91;'token'&#93;); ?>">
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
  <script src="todo.js"></script>
</body>
</html>
$(function() {
  'use strict';

   $('#new_todo').focus();
  // update
  $('#todos').on('click', '.update_todo', function() {
    // idを取得
    var id = $(this).parents('li').data('id');
    // ajax処理
    $.post('_ajax.php', {
      id: id,
      mode: 'update',
      token: $('#token').val()
    }, function(res) {
      if (res.state === '1') {
        $('#todo_' + id).find('.todo_title').addClass('done');
      } else {
        $('#todo_' + id).find('.todo_title').removeClass('done');
      }
    })
  });

  // delete
  $('#todos').on('click', '.delete_todo', function() {
    // idを取得
    var id = $(this).parents('li').data('id');
    // ajax処理
    if (confirm('are you sure?')){
      $.post('_ajax.php', {
        id: id,
        mode: 'delete',
        token: $('#token').val()
      }, function() {
        $('#todo_' + id).fadeOut(800);
      });
    }
  });

  // create
  $('#new_todo_form').on('submit', function() {
    // titleを取得
    var title = $('#new_todo').val();
    // ajax処理
    $.post('_ajax.php', {
      title: title,
      mode: 'create',
      token: $('#token').val()
    }, function(res) {
      // liを追加
      var $li = $('#todo_template').clone();
      $li
        .attr('id', 'todo_' + res.id)
        .data('id', res.id)
        .find('.todo_title').text(title);
      $('#todos').prepend($li.fadeIn());
      $('#new_todo').val('').focus();
    });
    return false;
  });

});
<?php

session_start();

require_once(__DIR__ . '/config.php');
require_once(__DIR__ . '/functions.php');
require_once(__DIR__ . '/Todo.php');

$todoApp = new \MyApp\Todo();

if ($_SERVER&#91;'REQUEST_METHOD'&#93; === 'POST') {
  try {
    $res = $todoApp->post();
    header('Content-Type: application/json');
    echo json_encode($res);
    exit;
  } catch (Exception $e) {
    header($_SERVER['SERVER_PROTOCOL'] . ' 500 Internal Server Error', true, 500);
    echo $e->getMessage();
    exit;
  }
}
<?php




namespace MyApp;

class Todo {
  private $_db;

  public function __construct() {

    $this->_createToken();

    try {
      $this->_db = new \PDO(DSN, DB_USERNAME, DB_PASSWORD);
      $this->_db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
    } catch (\PDOException $e) {
      echo $e->getMessage();
      exit;
    }
  }

  private function _createToken(){
    if (!isset($_SESSION['token'])){
      $_SESSION['token'] = bin2hex(openssl_random_pseudo_bytes(16));
    }
  }

  public function getAll() {
    $stmt = $this->_db->query("select * from todos order by id desc");
    return $stmt->fetchAll(\PDO::FETCH_OBJ);
  }

  public function post() {
    $this->_validateToken();
    if (!isset($_POST['mode'])) {
      throw new \Exception('mode not set!');
    }

    switch ($_POST['mode']) {
      case 'update':
        return $this->_update();
      case 'create':
        return $this->_create();
      case 'delete':
        return $this->_delete();
    }
  }

  private function _validateToken(){
    if (
      !isset($_SESSION['token']) ||
      !isset($_POST['token']) ||
      $_SESSION['token'] !== $_POST['token']
    ) {
      throw new \Exception('invalid token!');
    }
  }
  private function _update() {
    if (!isset($_POST['id'])) {
      throw new \Exception('[update] id not set!');
    }

    $this->_db->beginTransaction();

    $sql = sprintf("update todos set state = (state + 1) %% 2 where id = %d", $_POST['id']);
    $stmt = $this->_db->prepare($sql);
    $stmt->execute();

    $sql = sprintf("select state from todos where id = %d", $_POST['id']);
    $stmt = $this->_db->query($sql);
    $state = $stmt->fetchColumn();

    $this->_db->commit();

    return [
      'state' => $state
    ];

  }

  private function _create() {
    if (!isset($_POST['title']) || $_POST['title'] === '') {
      throw new \Exception('[create] title not set!');
    }

    $sql = "insert into todos (title) values (:title)";
    $stmt = $this->_db->prepare($sql);
    $stmt->execute([':title' => $_POST['title']]);

    return [
      'id' => $this->_db->lastInsertId()
    ];
  }

  private function _delete() {
    if (!isset($_POST['id'])) {
      throw new \Exception('[delete] id not set!');
    }

    $sql = sprintf("delete from todos where id = %d", $_POST['id']);
    $stmt = $this->_db->prepare($sql);
    $stmt->execute();

    return [];

  }
}