20200529のPHPに関する記事は18件です。

Laravel + SQLiteでシンプルなブログアプリを作っていく

はじめに

以前の投稿、Laravel + SQLite 準備編からの続きで、開発を進めます。

ドットインストール Laravel 5.5入門の内容に沿ってシンプルなブログアプリ開発を行います。

環境

VisualStudioCode

Laravel Framework 7.12.0

マイグレーションの設定

まず記事に関するモデルから作ります。

Terminal
$ php artisan make:model Post --migration

モデルはartisanコマンドを使えば良いのでphp artisanとします。
make:modelとしてモデル名は記事のModelなのでPostとします。

その後にバージョン管理するためのマイグレーションファイルも作るので、
--migrationというオプションを付けます。

こうするとModelが作られてmigrationファイルも作られます。


migrationファイルを開いてデータベース構造の設定をします。
migrationファイルはdatabaseの中のmigrationsにあります。
migrationファイルにはup()とdown()というメソッドがあります。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('body');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('posts');
    }
}

up()→このマイグレーションで行いたい処理
down()→それを巻き戻すための処理

これらがあることでデータベース構成を変更したり、その変更を取り消したりすることが出来るので、結果としてデータベース構成のバージョン管理ができるという仕組みです。

upメソッドは自動的にmodelを複数形にしたテーブル名にしてくれてidとtimestampsを設定してくれています。
idは連番で付けられる主キーです。

timestampsはcreated_at updated_atというcolumnを作ってくれて作成日時と更新日時を自動で管理してくれる仕組みになっています。

columnを追加したいので、titleとbodyというcolumnにします。

$table->id();
$table->string('title');
$table->text('body');
$table->timestamps();

columnの種類
SQLでいうところのvarcharはstringで設定していくのでstring('title')
bodyはテキストで設定したいのでtext('body')

参考:varchar

可変長の文字列です。可変長とは「長さが決まっていない」という意味で、varchar(10)に2バイトを格納すると、10バイトではなく2バイト使用します。
最大8,000バイト。文字データ型として最もよく使われるデータ型です。

このマイグレーションファイルの中身を実行します。

Terminal
$ php artisan migrate

SQLite でテーブルが作られたか確認。

Terminal
$ sqlite3 database/database.sqlite
Terminal
sqlite> .schema posts
Terminal
sqlite> .quit

確認できたらこれでOKです。

Modelのインタラクティブな操作

LaravelはEloquentモデルと呼ばれていてSQLを意識しなくても直感的にデータが操作できるようになっています。

Eloquent:利用の開始 5.7 Laravel - ReaDouble

Eloquent ORMはLaravelに含まれている、美しくシンプルなアクティブレコードによるデーター操作の実装です。それぞれのデータベーステーブルは関連する「モデル」と結びついています。モデルによりテーブル中のデータをクエリできますし、さらに新しいレコードを追加することもできます。

Postモデルを作ったのでphp artisan tinkerというコマンドを使いインタラクティブに操作してみます。

Terminal
$ php artisan tinker

まずはレコードを挿入します。
インスタンスを作りsaveメソッドを使います。

インスタンスの作り方はデフォルトで名前空間がAppになっているのでnew App\Post()とします。

名前空間の概要 - Manual - PHP

Terminal
>>> $post = new App\Post()
=> App\Post {#3054}

他にも設定していきます。

Terminal
>>> $post->title = 'title 1'; $post->body = 'body 1';
=> "body 1"
Terminal
>>> $post->save();
=> true
>>> 

このようにtrueと出れば正常に処理が終了したという意味になります。

格納したデータを確認する
APP\Post::all();
こちらのコマンドを使います。

Terminal
>>> APP\Post::all();
=> Illuminate\Database\Eloquent\Collection {#3781
     all: [
       App\Post {#3780
         id: "1",
         title: "title 1",
         body: "body 1",
         created_at: "2020-05-24 07:40:04",
         updated_at: "2020-05-24 07:40:04",
       },
     ],
   }

もっとシンプルな表示でみたい場合
App\Post::all()->toArray();

Terminal
>>> App\Post::all()->toArray();
=> [
     [
       "id" => 1,
       "title" => "title 1",
       "body" => "body 1",
       "created_at" => "2020-05-24T07:40:04.000000Z",
       "updated_at" => "2020-05-24T07:40:04.000000Z",
     ],
   ]

tinker終了
exit

Terminal
>>> exit
Exit:  Goodbye

SQLiteでも確認する

Terminal
$ sqlite3 database/database.sqlite
Terminal
sqlite> select * from posts;

先ほどのtitle1が入っているか確認

Terminal
1|title 1|body 1|2020-05-24 07:40:04|2020-05-24 07:40:04

Eloquentモデルを使えばSQLを特に意識しなくてもこのようにデータが扱えます。

Terminal
sqlite> .exit

終了

Mass Assignmentの設定

Eloquentモデルをいじっていきたいのでまずtinkerを使います。

Terminal
$ php artisan tinker

データを格納する際にインスタンスを作ってsaveとしていましたが、

Terminal
App\Post::create(['title'=>'title 2', 'body'=>'body 2']);

これでそれぞれにデータを与えて一気に追加することも可能です。
しかしそのまま実行しても以下のようなエラーになります。

Terminal
Illuminate/Database/Eloquent/MassAssignmentException with message 'Add [title] to fillable property to allow mass assignment on [App/Post].'

MassAssignment エラー: 意図しないリクエストによって悪意のあるデータが挿入されてしまう脆弱性

これを実行するにはLaravelでデフォルト設定を変える必要があります。

モデルで設定をします。
Modelはappの中にあるのでapp>Post.phpを編集します。

class Post extends Model {}の中に

Terminal
protected $fillable = ['title', 'body'];

このcolumnの、titleとbodyにデータを挿入していい という設定になります。

設定を変えたのでtinkerをexitしてもう一度tinkerで入ります。

Terminal
>>> exit
Terminal
$ php artisan tinker

これで先ほどと同じコマンドを実行できます。

Terminal
>>> App\Post::create(['title'=>'title 2', 'body'=>'body 2']);
=> App\Post {#3935
     title: "title 2",
     body: "body 2",
     updated_at: "2020-05-24 08:37:32",
     created_at: "2020-05-24 08:37:32",
     id: 2,
   }

さらにApp\Post::create(['title'=>'title 3', 'body'=>'body 3']);と追加すれば3も挿入できます。

Terminal
>>> App\Post::all()->toArray();
=> [
     [
       "id" => 1,
       "title" => "title 1",
       "body" => "body 1",
       "created_at" => "2020-05-24T07:40:04.000000Z",
       "updated_at" => "2020-05-24T07:40:04.000000Z",
     ],
     [
       "id" => 2,
       "title" => "title 2",
       "body" => "body 2",
       "created_at" => "2020-05-24T08:37:32.000000Z",
       "updated_at" => "2020-05-24T08:37:32.000000Z",
     ],
     [
       "id" => 3,
       "title" => "title 3",
       "body" => "body 3",
       "created_at" => "2020-05-24T08:39:36.000000Z",
       "updated_at" => "2020-05-24T08:39:36.000000Z",
     ],
   ]

データの抽出

特定のidのデータを引っ張ってくる場合はfindにidを渡します。
idが3のデータを引っ張ってくるには、

Terminal
>>> App\Post::find(3)->toArray();
=> [
     "id" => 3,
     "title" => "title 3",
     "body" => "body 3",
     "created_at" => "2020-05-24T08:39:36.000000Z",
     "updated_at" => "2020-05-24T08:39:36.000000Z",
   ]

条件付きで抽出するにはwhereとgetを使います。
例えばidが1より大きいものをgetせよという命令はこのようにします。

Terminal
>>> App\Post::where('id', '>', 1)->get()->toArray();
=> [
     [
       "id" => 2,
       "title" => "title 2",
       "body" => "body 2",
       "created_at" => "2020-05-24T08:37:32.000000Z",
       "updated_at" => "2020-05-24T08:37:32.000000Z",
     ],
     [
       "id" => 3,
       "title" => "title 3",
       "body" => "body 3",
       "created_at" => "2020-05-24T08:39:36.000000Z",
       "updated_at" => "2020-05-24T08:39:36.000000Z",
     ],
   ]

このように2と3が抽出されています。

データの並び替え

orderBy('created_at', 'desc')で新しい順に表示します。

Terminal
>>> App\Post::where('id', '>', 1)->orderBy('created_at', 'desc')->get()->toArray();
=> [
     [
       "id" => 3,
       "title" => "title 3",
       "body" => "body 3",
       "created_at" => "2020-05-24T08:39:36.000000Z",
       "updated_at" => "2020-05-24T08:39:36.000000Z",
     ],
     [
       "id" => 2,
       "title" => "title 2",
       "body" => "body 2",
       "created_at" => "2020-05-24T08:37:32.000000Z",
       "updated_at" => "2020-05-24T08:37:32.000000Z",
     ],
   ]

SQLでいうところのlimitで、変数を制限するにはtakeを使います。

SQLデータ分析入門#4『LIMIT句を理解する』

指定したレコードを上限に、結果を引っ張ってくる

Terminal
>>> App\Post::where('id', '>', 1)->take(1)->get()->toArray(); 
=> [
     [
       "id" => 2,
       "title" => "title 2",
       "body" => "body 2",
       "created_at" => "2020-05-24T08:37:32.000000Z",
       "updated_at" => "2020-05-24T08:37:32.000000Z",
     ],
   ]

データの更新

一旦Postの3を$postに入れます。

Terminal
>>> $post = App\Post::find(3);
=> App\Post {#4008
     id: "3",
     title: "title 3",
     body: "body 3",
     created_at: "2020-05-24 08:39:36",
     updated_at: "2020-05-24 08:39:36",
   }

$post->title を更新します。

Terminal
>>> $post->title = 'title 3 updated';
=> "title 3 updated"
Terminal
>>> $post->save();
=> true

これでデータベースに反映したので確認します。

Terminal
>>> App\Post::all()->toArray();
=> [
     [
       "id" => 1,
       "title" => "title 1",
       "body" => "body 1",
       "created_at" => "2020-05-24T07:40:04.000000Z",
       "updated_at" => "2020-05-24T07:40:04.000000Z",
     ],
     [
       "id" => 2,
       "title" => "title 2",
       "body" => "body 2",
       "created_at" => "2020-05-24T08:37:32.000000Z",
       "updated_at" => "2020-05-24T08:37:32.000000Z",
     ],
     [
       "id" => 3,
       "title" => "title 3 updated",
       "body" => "body 3",
       "created_at" => "2020-05-24T08:39:36.000000Z",
       "updated_at" => "2020-05-24T09:20:06.000000Z",
     ],
   ]

title 3 updatedに変わっているのでOKです。

データの削除

deleteを使います。

以下のように今$postがidが3のデータになっています。

Terminal
>>> $post
=> App\Post {#4008
     id: "3",
     title: "title 3 updated",
     body: "body 3",
     created_at: "2020-05-24 08:39:36",
     updated_at: "2020-05-24 09:20:06",
   }

これを削除します。

Terminal
>>> $post->delete();
=> true

全件表示で確認します。

Terminal
>>> App\Post::all()->toArray();
=> [
     [
       "id" => 1,
       "title" => "title 1",
       "body" => "body 1",
       "created_at" => "2020-05-24T07:40:04.000000Z",
       "updated_at" => "2020-05-24T07:40:04.000000Z",
     ],
     [
       "id" => 2,
       "title" => "title 2",
       "body" => "body 2",
       "created_at" => "2020-05-24T08:37:32.000000Z",
       "updated_at" => "2020-05-24T08:37:32.000000Z",
     ],
   ]

idが1と2だけになっています。

Controller

Postモデルにいくつかデータを格納できたので、そのデータをブラウザで表示します。
それにはどのURLでどのような処理をするかの設定をする必要があります。

その設定をルーティングと呼び、routesフォルダの中のweb.phpで設定できます。

デフォルトの以下の部分は今回は使わないので削ります。

routes>web.php
Route::get('/', function () {
    return view('welcome');
});

URLに/をつけてgetでアクセスした時のroutingを作る場合はまず、
Route::get('/', '')のように書いて、その後に行いたい処理を書いていきます。
PostsController@indexアクションを実行せよとしていきます。

routes>web.php
Route::get('/', 'PostsController@index');

Controllerが必要になるので作っていきます。
artisanコマンドを使うのでphp artisan make:controllerとしてPostsControllerという名前にします。

Terminal
$ php artisan make:controller PostsController
Controller created successfully.

Controllerが作られました。

Controllerはapp>Http>ControllersにPostsController.phpができているので編集していきます。

helloと表示してみます。
表示する内容を返せばいいのでreturn "hello";のようにします。

app>Http>Controllers>PostsController.php
extends Controller
{
  public function index() {
    return "hello";
  }
}

確認していきたいのでサーバーを立ち上げます。
サーバーを立ち上げるにあたってIPアドレスが必要になるので、
$ ifconfig で調べられます。
inetの隣に4つ並んでいるものがローカルIPです。

ホストを指定する必要があるので、先程のIPアドレスを入れて--host 192.XXX.X.XX -- portを8000番で立ち上げてます。

Terminal
 $ php artisan serve --host 192.168.X.XXX --port 8000

実行

Terminal
Laravel development server started: http://192.168.X.XXX:8000

アクセス先のURLが出てきますので確認します。

helloと出てくればOKです。

以上がroutingを設定してその処理をController辺りに作成するという流れでした。

View

viewを用意してそこにデータを入れていきます。
resourcesの中のViewsの中に作ります。

フォルダ名:Postに関するviewをまとめるのでpostsフォルダを用意します。
その中にAction名に対応したviewの名前を付けます。

ファイル名:indexアクションなのでindex。LaravelではBladeというテンプレートエンジンが使えるので、index.blade.phpとなります。

Laravel 5.5 Bladeテンプレート

BladeはシンプルながらパワフルなLaravelのテンプレートエンジンです。他の人気のあるPHPテンプレートエンジンとは異なり、ビューの中にPHPを直接記述することを許しています。全BladeビューはPHPへコンパイルされ、変更があるまでキャッシュされます。つまりアプリケーションのオーバーヘッドは基本的に0です。Bladeビューには.blade.phpファイル拡張子を付け、通常はresources/viewsディレクトリの中に設置します。

内容の編集:emmetの機能でhtml5のテンプレートを貼り付けたら、titleと文字コードを編集。

bodyの方では、中身を後で中央揃えにします。
<div class="container">で囲っておき、見出し(h1)と記事の一覧(li)を作っていきます。

resources>views>posts>index.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Blog Posts</title>
</head>
<body>
  <div class="container">
    <h1>Blog Posts</h1>
    <ul>
      <li><a href="">title</a></li>
      <li><a href="">title</a></li>
      <li><a href="">title</a></li>
    </ul>
  </div>
</body>
</html>

これでviewができたのでControllerの方でこのviewを指定します。

app>Http>Controllers>PostsController.php
extends Controller
{
  public function index() {
    // return "hello"; 編集
    return view('posts.index');
  }
}

viewを使うにはreturn viewの後にviewの名前を指定します。
Action名と同じにしたのでposts.indexとします。
フォルダの区切りは .(ドット) になっている点に注意します。

ブラウザをリロードして確認

スクリーンショット 2020-05-25 10.22.44.png
このようにviewが反映されてればOKです。

データの抽出

データを入れるため、まずはデータの取得からしていきます。

データを取得するにはEloquentの命令を使います。
$posts = \App\Post::all();
とすると全件を取得することが出来ます。
しかし名前空間が長くなるので、上の方でuse App\Post;を使います。

app>Http>Controllers>PostsController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Post;

class PostsController extends Controller
{
  public function index() {
    $posts = Post::all();
    dd($posts->toArray()); // dump die
    return view('posts.index');
  }
}

データが取得できたかどうかは、Laravelで用意されているdd()という命令を使って確かめます。
ddはdump dieの略で結果を出力して処理をその場で終了する命令です。
配列で見やすく表示させたいのでdd($posts->toArray());としています。
スクリーンショット 2020-05-25 10.45.18.png
こうしてブラウザで確認するとtitle1とtitle2のデータが取れています。

あとは記事を新しい順に並べたいのでorderByで並べ替えます。
Post::orderBy('created_at', 'desc')->get();

これでいいのですが、このcreated_atで新しい順に取ってくるという処理はよく行うので、
Laravelではlatest()という書き方も用意されていて
Post::latest()->get();で同じ意味になります。

app>Http>Controllers>PostsController.php
class PostsController extends Controller
{
  public function index() {
    // $posts = Post::orderBy('created_at', 'dest')->get;
    $posts = Post::latest()->get();
    dd($posts->toArray()); // dump die
    return view('posts.index');
  }
}

ブラウザで確認します。
スクリーンショット 2020-05-25 11.01.12.png
title2のほうが上に来ているのがわかります。
これでデータの抽出ができました。

データをViewに埋め込む

$postsでデータの取得ができたのでこれをviewの方に渡していきます。
return view()の第2引数に渡します。

'posts' => $postsとすると$postsの内容をviewの中でpostsという名前で使うことが出来ます。

もしくはwith('post', '$posts');でも全く同じ意味になります。

class PostsController extends Controller
{
  public function index() {
    $posts = Post::latest()->get();
    // return view('posts.index', ['posts' => $posts]);
    return view('posts.index')->with('posts', $posts);
  }
}

viewの方でこのpostsを使っていきます。

Bladeでは@foreachという制御構造を使えるので@foreach(\$posts as \$post)として、この$postを使ってループを展開していきます。

resources>views>posts>index.blade.php
<div class="container">
    <h1>Blog Posts</h1>
    <ul>
      @foreach ($posts as $post)
      <li><a href="">{{ $post->title }}</a></li>
      @endforeach
    </ul>
  </div>

これで$postsが無くなるまで$postでひとつひとつの記事を表すことが出来るので値を埋め込みます。

値の埋め込みには二重波括弧を使うとエスケープもします。
{{ $post->title }}で$postのtitleを表示をします。

ブラウザを確認します。

スクリーンショット 2020-05-25 11.25.45.png

title2, title1 となっています。

もしこちらの$postsの中身が空だったら、と書きたい場合

@forelseという構文を使います。
そうすると、@emptyという命令が使えるので@emptyの後に、このデータが空だった時のテンプレートを入れ込むことが出来ます。

No posts yetと表示させることにします。

  <div class="container">
    <h1>Blog Posts</h1>
    <ul>
      @forelse ($posts as $post)
      <li><a href="">{{ $post->title }}</a></li>
      @empty
      <li>No posts yet</li>
      @endforelse
    </ul>
  </div>

一旦$postsを空にします。

class PostsController extends Controller
{
  public function index() {
    // $posts = Post::latest()->get();
    $posts = [];
    // return view('posts.index', ['posts' => $posts]);
    return view('posts.index')->with('posts', $posts);
  }
}

スクリーンショット 2020-05-25 11.43.00.png
空のときの表示ができました。
確認できたら$posts = [];は戻しておきます。

BladeではHTMLに出力したくないコメントは{{-- --}}で囲みます。

  <div class="container">
    <h1>Blog Posts</h1>
    <ul>
      {{--
      @foreach ($posts as $post)
      <li><a href="">{{ $post->title }}</a></li>
      @endforeach
      --}}
      @forelse ($posts as $post)
      <li><a href="">{{ $post->title }}</a></li>
      @empty
      <li>No posts yet</li>
      @endforelse
    </ul>
  </div>

スタイルを整える
CSSを読み込むリンクタグを入れます。
<link rel="stylesheet" href="/css/styles.css">

CSSやJavaScriptや画像はpublicフォルダの中に作っていきます。

public>css>styles.css
body {
  font-family: Verdana, sans-serif;
  font-size: 14px;
}

.container {
  width: 400px;
  margin: 20px auto;
}

h1 {
  font-size: 16px;
  padding-bottom: 10px;
  margin-bottom: 15px;
  border-bottom: 1px solid #ddd;
}

ul >li {
  margin-bottom: 5px;
}

以下のような見た目になればOKです。
スクリーンショット 2020-05-25 13.00.12.png

記事の詳細画面の作成

記事の一覧ができたので、次は記事の詳細画面を作ります。
まずはroutingです。

URLとしてはgetでアクセスされたpostsの1や2を処理したいのですが、
ここにきた値をControllerに渡したい場合は波括弧を付けて変数名を書きます。

Controllerはshowアクションに渡します。
Route::get('/posts{id}', 'PostsController@show');

routes>web.php
<?php

use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', 'PostsController@index');
Route::get('/posts{id}', 'PostsController@show');

次にshowアクションを作ります。

Controllerの中に続きから書いていきます。

app>Http>Controllers>PostsController.php
public function index($id) {
    // $post = Post::find($id);
    $post = Post::findOrFail($id);
    return view('posts.show')->with('post', $post);
  }

web.phpのURLのパラメーターから渡ってきた値は引数で受け取ることが出来るので$idとします。

データを引っ張ってくるのでPost::find($id)でいいのですが$idでデータが見つからなかった場合に、例外を返したいのでその場合はfindOrFail()という命令を使います。

そしてデータをviewに渡してあげれば良いので今回はposts.showというテンプレートに対してpostという名前で$postのデータを渡します。

app>Http>Controllers>PostsController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Post;

class PostsController extends Controller
{
  public function index() {
    $posts = Post::latest()->get();
    return view('posts.index')->with('posts', $posts);
  }
  public function show($id) {
    $post = Post::findOrFail($id);
    return view('posts.show')->with('post', $post);
  }
}

viewを作りたいのでresourcesフォルダに行きます。

index.blade.phpを複製してshow.blade.phpを作り編集していきます。

head内タイトル変更

<title>{{ $post->title }}</title>

body内

resources>views>posts>show.blade.php
<body>
  <div class="container">
    <h1>{{ $post->title }}</h1>
    <p>{!! nl2br(e($post->body)) !!}</p>
  </div>
</body>

この部分について
<p>{!! nl2br(e($post->body)) !!}</p>

本文の方は改行が入ってくる可能性もあるので、改行をbrタグに変換したいです。
まずは二重波括弧ではなく、中身をエスケープしないで値を出力するための{{!! !!}}という命令を使います。

$post->bodyを入れたいので、一旦Laravelのe()ヘルパーでエスケープして、それをnl2br()で挟み改行をbrタグにします。


e()
https://readouble.com/laravel/5.5/ja/helpers.html#method-e

e関数は、PHPのhtmlspecialchars関数をdouble_encodeオプションにfalseを指定し、実行します。

echo e('<html>foo</html>');

// &lt;html&gt;foo&lt;/html&gt;

e()について
https://laraweb.net/knowledge/835/

HTMLエンティティ―(※ブラウザがHTMLとして処理せずにそのまま出力させるための代替コード)の実行

echo e('<strong>laravel</strong>');
//出力結果 ⇒ &lt;strong&gt;laravel&lt;/strong&gt;

nl2br()
https://www.php.net/manual/ja/function.nl2br.php

nl2br — 改行文字の前に HTML の改行タグを挿入する
説明
nl2br ( string \$string [, bool $is_xhtml = TRUE ] ) : string
string に含まれるすべての改行文字 (\r\n、 \n\r、\n および \r) の前に 
 あるいは 
 を挿入して返します。

Implicit Binding

記事の一覧から記事の詳細画面にリンクを張ります。

routingの設計通り/posts/{{ $post->id }}でもいいですが、

@forelse ($posts as $post)
li><a href="/posts/{{ $post->id }}">{{ $post->title }}</a></li>

URLを生成するための命令が他にもいくつかあります。

url()という命令を使う方法
php
<li><a href="{{ url('/$posts', $post->id }}">{{ $post->title }}</a></li>

これでも同じ意味になります。

もしくはControllerとActionからURLを生成することができるaction()という命令もあります。
その場合はPostsControllerのshowに対応するURLを生成する、という書き方ができます。

routingのパラメーターに渡す値は、第2引数以降に入れていけばいいので以下のようにします。

<li><a href="{{ action('PostsController@show', $post->id }}">{{ $post->title }}</a></li>

これで各titleをクリックすると以下のように詳細リンクに飛べるようになりました。
スクリーンショット 2020-05-25 14.49.32.png

URLから$idを受け取って、Controllerでその$idを元にモデルを引っ張ってくるという流れはよく行うので、暗黙的にモデルをデータに結びつけられるImplicit Bindingという仕組みも用意されています。

まずroutingを'/posts/{post}'にします。

routes>web.php
// Route::get('/posts{id}', 'PostsController@show');
Route::get('/posts{post}', 'PostsController@show');
app>Http>Controllers>PostsController.php
// public function show(Post $id) {
public function show(Post $post) {

これで自動的にこの$postにはURLから受け取ったidに対応するデータが格納されます。

なのでこの場合はidで引っ張ってくる必要がないのでこれでOKです。

  public function show(Post $post) {
    // $post = Post::findOrFail($id); // 削除
    return view('posts.show')->with('post', $post);
  }

さらにリンクの生成についても$post->id$postにするだけで、@postのidをパラメーターに渡してくれます。

resources>views>posts>index.blade.php
      <!-- <li><a href="{{ action('PostsController@show', $post->id) }}">{{ $post->title }}</a></li> -->
      <li><a href="{{ action('PostsController@show', $post) }}">{{ $post->title }}</a></li>

同じようにtitleから詳細リンクへの動作を確認することができます。
以上がImplicit Bindingです。

長くなるので続きは分けます。

後半は共通部分の部品化、編集やコメント機能などから進めていきます。

Links

ドットインストール Laravel 5.5入門
https://dotinstall.com/lessons/basic_laravel_v2

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Create Simple PHP 7 CRUD REST API with MySQL & PHP PDO

This is a step by step PHP 7 & MySQL REST API tutorial, In this tutorial i am going to share with you how to create a PHP 7 CRUD (Create, Read, Update, Delete) RESTful API with MySQL database.

click here to read more:
https://www.positronx.io/create-simple-php-crud-rest-api-with-mysql-php-pdo

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

How to Easily Check the PHP Version

The easiest method is to check the PHP version is the phpinfo() method and via the PHP command line tool.

click here to read more:

https://www.positronx.io/how-to-easily-check-the-php-version/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

How to Easily Check the PHP Version

The easiest method is to check the PHP version is the phpinfo() method and via the PHP command line tool.

click here to read more:
https://www.positronx.io/how-to-easily-check-the-php-version/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP 7 Upload & Store File/Image in MySQL Database Tutorial

how to upload files and images in MySQL Database. and how to implement file upload validation before sending it to a web server.

click here to read more:
https://www.positronx.io/php-upload-store-file-image-in-mysql-database/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP 7 Multiple Files/Images Upload in MySQL Database

PHP 7 Multiple Files and Images uploading tutorial, and we will learn to store uploaded files in the MySQL database along with some necessary file uploading validation.

click here to read more:
https://www.positronx.io/php-multiple-files-images-upload-in-mysql-database/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP Slim3フレームワークのサンプルアプリを作ろう(skeleton project)

はじめに

PHP Slim3フレームワークの勉強のため、
Slim公式のユーザーガイドにあるskeleton projectのサンプルアプリを作成します。

前提

下記記事で構築した環境を前提とします。

使用ツール

  • Tera Term

PHP Slim3フレームワークとは?

PHPのマイクロフレームワークのひとつ。
必要最低限の機能のみを提供し、その分速度が出るようにデザインされた軽量なフレームワークで、
同時アクセス数が比較的多く見込まれる場合に使用されるケースが多いそうです。

slim/slim-skeletonパッケージとは?

今回作成するskeleton projectのサンプルアプリは
slim/slim-skeletonパッケージを使用して作成します。
slim/slim-skeletonパッケージはプロジェクト構成のひな型です。

手順

1. 専用のユーザーを作成
2. プロジェクトディレクトリを作成
3. プロジェクトを作成
4. アプリケーションを動作させてみる

やってみよう

1. 専用のユーザーを作成

Composerをrootユーザーで操作することは推奨されていないので、
以下のコマンドでslimuserというユーザーを作成します。

useradd slimuser

作成したslimuserに以下のコマンドで切り替えます。

su slimuser

2. プロジェクトディレクトリを作成

slimuserユーザーが作成されると
/home/ディレクトリ内にslimuserディレクトリが作成されます。
その中に以下のプロジェクトディレクトリを作成します。

/home/slimuser/projects/slim/

3. プロジェクトを作成

上記で作成したSlim-Skeletonディレクトリまで移動します。

cd /home/slimuser/projects/slim/

以下のコマンドを実行します。

composer create-project slim/slim-skeleton Slim-Skeleton

Slim-Skeletonディレクトリが生成されました。

/home/slimuser/projects/slim/Slim-Skeleton

Slim-Skeletonディレクトリ内には以下のようなファイル群が生成されます。

[slimuser@localhost Slim-Skeleton]$ ls -la
total 124
drwxrwxr-x  9 slimuser slimuser   278 May 29 09:08 .
drwxrwxr-x  3 slimuser slimuser    27 May 29 09:06 ..
drwxrwxr-x  2 slimuser slimuser   114 Oct  5  2019 app
-rw-rw-r--  1 slimuser slimuser  1245 Oct  5  2019 composer.json
-rw-rw-r--  1 slimuser slimuser 91178 May 29 09:08 composer.lock
-rw-rw-r--  1 slimuser slimuser   583 Oct  5  2019 CONTRIBUTING.md
-rw-rw-r--  1 slimuser slimuser    33 Oct  5  2019 .coveralls.yml
-rw-rw-r--  1 slimuser slimuser   345 Oct  5  2019 docker-compose.yml
-rw-rw-r--  1 slimuser slimuser    51 Oct  5  2019 .gitignore
drwxrwxr-x  2 slimuser slimuser    23 Oct  5  2019 logs
-rw-rw-r--  1 slimuser slimuser   883 Oct  5  2019 phpunit.xml
drwxrwxr-x  2 slimuser slimuser    40 Oct  5  2019 public
-rw-rw-r--  1 slimuser slimuser  1450 Oct  5  2019 README.md
drwxrwxr-x  5 slimuser slimuser    61 Oct  5  2019 src
drwxrwxr-x  5 slimuser slimuser   102 Oct  5  2019 tests
-rw-rw-r--  1 slimuser slimuser   475 Oct  5  2019 .travis.yml
drwxrwxr-x  3 slimuser slimuser    19 Oct  5  2019 var
drwxrwxr-x 22 slimuser slimuser   325 May 29 09:08 vendor

4. アプリケーションを動作させてみる

今回は、PHPのビルトインWebサーバーを使用して表示します。
以下のコマンドを実行します。
192.168.33.60部分は各自の環境に置き換えてください。

php -S 192.168.33.60:8080 -t public public/index.php

下記が表示されたらブラウザでhttp://192.168.33.60:8080を開いてみてください。

[slimuser@localhost Slim-Skeleton]$ php -S 192.168.33.60:8080 -t public public/index.php
PHP 7.1.33 Development Server started at Fri May 29 10:02:43 2020
Listening on http://192.168.33.60:8080
Document root is /home/slimuser/projects/slim/Slim-Skeleton/public
Press Ctrl-C to quit.

このように"Hello world!"とブラウザに表示されます。
skeleton-project.png

コマンドのオプションの解説

①-S 192.168.33.60:8080
ビルトインWEBサーバをこのURL(192.168.33.60:8080)で立ち上げるという意味です。
②-t public public/index.php
ドキュメントルートや読み込むファイルを指定します。

このオプションの使い方がわからず、apacheのDocumentRootに
プロジェクトを置こうとしたけど、rootユーザーじゃないと権限の問題で置けなくて、
どうすればいいかだいぶ途方に暮れていました。

PHPのビルトインWebサーバーではなく、apacheを使用するときは、
DocumentRootを書き換えることになるのでしょうね。

疑問点

参考サイトさんのこことかここで見た、↓みたいな
大きくSlimって出てるような画面が映らなかったの、なんでなんでしょうか...
同じことしてるのに...
image.png

参考サイト

slimphp/Slim-Skeleton
私家版 Slim Framework チュートリアル (1) 〜 特徴と準備編
PHP マイクロフレームワーク Slim インストール
PHPのマイクロフレームワーク「Slim」を使ってみたら本当にスリムだった話

関連ページ

Windows10にVagrantをを入れてCentOS7をインストールしよう

1. VagrantインストールからVagrantfileを設置まで
2. 仮想マシンの操作
3. WinSCP、Tera Termに秘密鍵でログイン
4. WinSCP、Tera Termにrootユーザーでパスワードログイン
5. zip/unzipをインストール
6. Vagrantにて仮想環境を配布

ローカルでLAMP環境を構築しよう

0. 事前準備
1. Apacheをインストール
2. MySQLをインストール
3. PHPをインストール
4. ファイアウォールとか停止する

Composerをインストール

CentOS7にComposerをインストールしよう

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Build PHP 7 User Registration (Signup) Form with MySQL

Learn how to create a basic PHP 7 user registration system, add PHP validation in registration form, securely hash password and store the user information in the MySQL database.

click here to read more:
https://www.positronx.io/build-php-user-registration-signup-form-with-mysql/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【SEO】【構造化データ】LocalBusiness

LocalBusiness

以下の実装をしたケースの紹介です。
https://developers.google.com/search/docs/data-types/local-business?hl=ja

コード

今回はこちらのページに実装した例です。
このページは電気自動車の充電スポット情報のページになります。
テンプレート側で変数をechoしています。

structuredData.template
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "LocalBusiness",
  "image": [
    "<?php echo $point_img; ?>"
  ],
  "@id": "<?php echo $point_pageUrl; ?>#LocalBusiness",
  "name": "<?php echo str_replace('"', '\"', $point_name); ?> EV電気自動車 充電スタンド",
  "address":
  {
    "streetAddress":"<?php echo $point_postal; ?>"
  },
  "description":"<?php echo str_replace('"', '\"', $point_name); ?>の充電スタンド情報ページです。利用可能時間や料金、プラグ形状、そしてユーザーの口コミなど情報満載!",
  "geo": {
    "@type": "GeoCoordinates",
    "latitude": <?php echo $point_latitude; ?>,
    "longitude": <?php echo $point_longitude; ?>
  },
  "url": "<?php echo $point_pageUrl; ?>",
  "telephone": "<?php echo $point_phoneNumber; ?>",
  "openingHours": ["Mo-Fr <?php echo $point_openingTimeWkStr; ?>", "Sa <?php echo $point_openingTimeSaStr; ?>", "Su <?php echo $point_openingTimeSuStr; ?>"]
}
</script>

テストツールで見るとこんな感じです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【SEO】【構造化データ】Product

Product

以下の実装をしたケースの紹介です。
https://developers.google.com/search/docs/data-types/product?hl=ja

今回のページの場合は売り物ではなく全国の車のカタログのため推奨フィールドは空が多いです。
ただこのようなケースでも必須フィールドを埋めれば適用することができます。

コード

製品が1つしかなかったのでテンプレート側で変数をechoする対応にしました。
今回はこちらのページに実装した例です。

製品が複数の場合の実装はこちらをご参照ください。

structuredData.template
<script type="application/ld+json">
[
  {
    "@context" : "https://schema.org",
    "@type" : "Product",
    "name" : "<?php echo $name;?>",
    "image" : "<?php echo $imagePath;?>",
    "url" : "<?php echo $url;?>",
    "brand" : {
      "@type" : "Brand",
      "name" : "<?php echo str_replace('"', '\"', $brand);?>"
    },
    "offers": {
      "@type" : "Offer",
      "price" : "<?php echo $price;?>",
      "priceCurrency" : "JPY"
    }
  }
]
</script>

テストツールで見るとこんな感じです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者に捧げる〜PHPを使って九九の表を作ろう〜

目次

・対象読者
・完成イメージ
・コードの記述・解説
・補足
・まとめ

対象読者

・progateやドットインストールでPHPを学習したばかりの方
・PHPの基礎の基礎を覚えた方
・PHPで何を作るか決まってない方

完成イメージ

スクリーンショット 2020-05-29 15.58.34.png
〜チェックポイント〜

・周りの1~9は目立つようにする
・偶数にだけ色をつける(今回はifを使います)
・tableタグなどを使い、表のようにする

コードの記述・解説(HTMLとPHP)

index.php
<h1>九九の表(今回はforを使います)</h1>
    <table>
        <tr>
            <!-- &nbsp;は半角スペースの文字実体参照(ただのスペースではhtmlは無として扱うから) -->
            <th>&nbsp;</th>
            <th>1</th>
            <th>2</th>
            <th>3</th>
            <th>4</th>
            <th>5</th>
            <th>6</th>
            <th>7</th>
            <th>8</th>
            <th>9</th>

            <?php
                for($i = 1;$i <= 9;$i++){
                    echo '<tr>';
                    // $iに格納した数字を表の見出しとして出力する
                    echo '<th>' . $i . '</th>';

                    // $iが1の時に$jは1~9全て掛けて<td>として出力(以下$iが9になるまで行う)
                    for($j = 1;$j <= 9;$j++) {
                        $result = $i * $j;
                        // 偶数の時だけbackgroundをつけて見やすくします
                        if($result % 2 === 0) {
                           echo '<td class="even">' . $result . '</td>';
                        }
                        // 奇数はそのまま表示(elseはだと見辛くなるのであえて使わない)
                        if($result % 2 === 1) {
                            echo '<td>' . $result . '</td>';
                        }


                    }

                    // "\n"は改行のエスケープ文字(9まで計算したら改行する)
                    echo '</tr>'  . "\n";
                }
            ?>
        </tr>
    </table>

コードの記述・解説(CSS)

table {
    /* 隣接するセルのボーダーを重ねて表示する */
    border-collapse: collapse;
}
th, td {
  width: 100px;
  border: 1px solid limegreen;

}
th {
  background: aquamarine;
}
td {
  text-align: center;
}
.even {
    background-color: aqua;
}

補足(コード内に書くと長くなるコメント)

<!--
    printでも表示できるよ?
    A.  echo と print の最も大きなちがいは式( expression )かどうか 
    PHP の仕様として「 echo は式ではなくて、 print は式である 」
    ※echo にできて print にはできないことがあります。 それは「複数の引数を受け取ること」です。 
    ・echo は文( statement )。他の式や文の一部として使うことができない。戻り値を持たない。
    ・print は式( expression )。他の式や文の一部として使うことができる。戻り値を持っていて、それはいつも 1 。
    今回はどちらでもOKなのでprogateで使っているechoにしました。

    "\n"は必ずダブルクオーテーションで囲む
    なぜならシングルクオーテーションで囲むとただの文字列として出力されるから

    <tr></tr>:表でつくられる横一列のデータ(行)をまとめる。
    <th></th>:セルの要素であり、表の見出しを意味します。
    <td></td>:セルの要素であり、表の値(データ)を意味します。

-->

まとめ

自分はPHPを学習してまだ2日なので早速アウトプットしようと思い、記事を作成させていただきました!
コードなどに間違いがあれば是非お教えください?‍♂️
この記事を見たあなたの成長を応援します!!

~この記事も一緒にどうですか?~
初心者に捧げるハンバーガーメニューの作り方
初心者に捧げるヘッダーの作り方
Vue.jsで作る!自動保存するToDoリスト~その1~

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SEO【構造化データ】Product(複数)

Product

以下の実装をしたケースの紹介です。
https://developers.google.com/search/docs/data-types/product?hl=ja

今回のページの場合は売り物ではなく全国の車のカタログのため推奨フィールドは空が多いです。
ただこのようなケースでも必須フィールドを埋めれば適用することができます。

コード

テンプレート側にベタ書きでも良かったのですがテンプレートはなるべくスッキリさせたいのと元々裏側でforeachしていたのでそこに入れ込みたく、値を変数に入れてテンプレート側で表示するようにしました。

今回はこちらのページに実装した例です。

structuredData.php
foreach($cars as $key=>$car){
  $structuredData[] = array(
    "@context" => "https://schema.org",
    "@type" => "Product",
    "name" => $car['Name'],
    "image" => $car['ImagePath'],
    "url" => $car['PageUrl'],
    "brand" => array(
      "@type" => "Brand",
      "name" => $car['BrandName']
    ),
    "offers" => array(
      "@type" => "Offer",
      "price" => $car['Price'],
      "priceCurrency" => "JPY"
    ),
  );
}
$structuredData = json_encode($structuredData);
structuredData.template
<script type="application/ld+json">
<?php
if(isset($structuredData) && !empty($structuredData)){
    if($structuredData != '[]'){
        echo $structuredData;
    }
}
?>
</script>

テストツールで見るとこんな感じです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【SEO】【構造化データ】Product(複数)

Product

以下の実装をしたケースの紹介です。
https://developers.google.com/search/docs/data-types/product?hl=ja

今回のページの場合は売り物ではなく全国の車のカタログのため推奨フィールドは空が多いです。
ただこのようなケースでも必須フィールドを埋めれば適用することができます。

コード

テンプレート側にベタ書きでも良かったのですがテンプレートはなるべくスッキリさせたいのと元々裏側でforeachしていたのでそこに入れ込みたく、値を変数に入れてテンプレート側で表示するようにしました。

今回はこちらのページに実装した例です。

structuredData.php
foreach($cars as $key=>$car){
  $structuredData[] = array(
    "@context" => "https://schema.org",
    "@type" => "Product",
    "name" => $car['Name'],
    "image" => $car['ImagePath'],
    "url" => $car['PageUrl'],
    "brand" => array(
      "@type" => "Brand",
      "name" => $car['BrandName']
    ),
    "offers" => array(
      "@type" => "Offer",
      "price" => $car['Price'],
      "priceCurrency" => "JPY"
    ),
  );
}
$structuredData = json_encode($structuredData);
structuredData.template
<script type="application/ld+json">
<?php
if(isset($structuredData) && !empty($structuredData)){
    if($structuredData != '[]'){
        echo $structuredData;
    }
}
?>
</script>

テストツールで見るとこんな感じです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

学習記録 PHP#5(SQLコマンドの基礎)

SQLコマンドの基礎(MySQL用)

  • SQL:データベースに対する指示命令系統の統一規格
  • SQLコマンド:SQL規格に従って作られたコマンド、指示命令系統のこと

SQLコマンド記述時の注意点

  • SQLコマンドは大文字でも小文字でも可 列名(フィールド名)や表明(テーブル名)の大文字小文字は区別する。
  • 各キーワードは半角スペースで区切る
  • 半角スペースやカンマは改行コードでも代用可
  • コマンドは複数行に分けてもOK
  • ISO標準のSQLは最後にセミコロン「;」が必要
  • フィールド名やテーブル名意外はスペースも含めて必ず半角文字で記述する
  • フィールド名やテーブル名が誤認識される場合には前後にバッククォート「`」を付ける
  • SQLはRDBMSによって微妙に違いがある(方言のようなもの)

SELECTコマンドの基本的な書き方

SELECTコマンドの基本的な書き方

select フィールド名 from テーブル名 指定したテーブルから指定したフィールドを表示
select フィールド名,フィールド名 from テーブル名 フィールドが複数存在する場合はフィールド名をカンマで区切る
select * from テーブル名 「*」は全フィールドを表す

別名(フィールドやテーブルに別の名前を設定)

select フィールド名 as 別名
from テーブル名 as 別名 フィールドやテーブル名に別名を設定
  • テーブル名やフィールド名は原則英語で書く。 ただし表示の際は別名により日本語表示も可。

並べ替え

  • 並べ替え:「order by」キーワード
select フィールド名 from テーブル名 order by フィールド名 asc 指定したフィールドで昇順にする。ascは省略可のため省略することが多い。
select フィールド名 from テーブル名 order by フィールド名 desc 指定したフィールドで降順にする。
  • 複数条件での並べ替え
select フィールド名 from テーブル名 order by フィールド名 asc, フィールド名 desc 複数フィールドでの並べ替えはカンマで区切って記述。
  • 並べ替えの際は「Null」は最小値として扱われる

抽出条件指定

  • 抽出条件指定:where
select フィールド名 from テーブル名 where 条件式 条件式は「フィールド名」「比較演算子」「値」を使用して記述
  • 比較演算子
= 等しい <>
> 左辺が右辺より大きい <
>= 左辺が右辺より大きいか等しい <=
  • whereとorder byは同時に使用可能
    whereで先に抽出条件を指定し、その後order byで並べ替える。

  • 複合条件(AND条件・OR条件)

select フィールド名 from テーブル名 where 条件式A and B 条件式Aと条件式Bのand条件
select フィールド名 from テーブル名 where 条件式A or B 条件式Aと条件式Bのor条件
  • IN(OR条件の複数同時指定)
select フィールド名 from テーブル名 where フィールド名 in (A, B, C) フィールド名のデータがAorBorCのレコードを抽出。

リテラルと識別子

  • リテラル:固定値 条件式におけるフィールド名=10の場合、10のこと
where フィールド名 = '文字列リテラル' 文字列リテラルの前後はシングルクォーテーション「'」で囲む
where フィールド名 = '日付時刻リテラル' 日付時刻リテラルの前後はシングルクォーテーション「'」で囲む。「yyyy/M/d h: m: s」「yyyy-M-d h: m: s」で指定。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

イケてないswitch-case文からおさらばするPHP生活の知恵

PHPの等価比較がゆるふわなことにお悩みのぺちぱーのみなさまこんにちは。
PHPのswitch-case文が「緩やかな比較」であることは有名ですが、なるべくならいつも厳密に比較したい諸兄にはツラい現実を投げかけてきていると思います。

イケてない例

たとえば、Laravel案件とかでこんな処理を見かけることがあると思います。

class HogeController {

    public function fuga($method){
        $title = '';

        switch($method){
            case 'new':
                $title = '新規作成';
                break;
            case 'update':
                $title = '更新';
                break;
        }

        return view('fuga', compact('title'));
    }

}

きっとルートパラメータで来た文字列でタイトル出し分けたいんでしょう。
こんなことでswitch分岐させる?みたいな微妙に気分になりますね。
もうちょっと違う方法でできそうな気がしますよね。

連想配列を使ってみよう

これくらいだったら連想配列で行けそうですよね。いちいち真偽判定しない分、こちらの方がパフォーマンスが良いと言われています。(ベンチマークは各々やってくだせえ)

class HogeController
{
    public function fuga($method)
    {
        $title = [
            'new' => '新規作成',
            'update' => '更新'
        ][$method];

        return view('fuga', compact('title'));
    }
}

この例だと $methodnewupdate 以外が来てしまうと死ぬのですが、そんなのが来そうなときは array_key_exists() でチェックすると良いでしょう。

こんな感じ。

class HogeController
{
    public function fuga($method)
    {
        $title_list = [
            'new' => '新規作成',
            'update' => '更新'
        ];

        $title = (array_key_exists($method, $title_list))
            ? $title_list[$method]
            : '';

        return view('fuga', compact('title'));
    }
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

時間を知らせてくれる秘書が欲しい

概要

いつもよくお世話になる音声では
事務的な冷たい視線で社長に「社長、XX時に--の予定が入っています」
「社長、--の予定まで、あとXX分です」
と社長気分をあじわいながらあれやこれやじゅるじゅるやらを行ってくれる。

「そんな気分をちょっとでも味わいたい」

ということで、今回は予定を登録後、
登録時間になったらお知らせしてくれる
システムをPHPの練習がてら作った。

処理の流れ

予定をwebにて登録

Webhookを使用しTeamsに通知

情報をwebサーバーに保存

10分に一度CRONに登録したプログラムを起動

Webhookを使用しTeamsに通知

TeamsのWebhookの設定の流れ

1.Teamsにて自分だけのチャンネルを作成し
 設定から「コネクタ」を選択
2020-05-29_09h37_48 (2).png

2.「incoming Webhook」にて「追加」をクリック
 ※一回追加したため「構成」になっている

2020-05-29_09h38_10 (2).png

3.適当な名前を付けて「作成」をクリック
 その後表示されるURLが必要なため、どこかに保存
 2020-05-29_09h43_05 (2).png

予定ページ

予定を登録するページは以下のようになっている

2020-05-29_09h26_15.png

すごく質素、、、

このページは予定とその時間を登録するようになっている
登録された予定は登録ボタンの下にスタックされていく

また、登録情報はWebサーバー上にJSON形式で保存され、
そのデータを表示させている。

コードは以下となっている。

todo.php
<?php
function h($v){
    return htmlspecialchars($v, ENT_QUOTES, 'UTF-8');
}

$FILE = 'todo.txt';
$id = uniqid(); 

$DATA = []; 
$BOARD = []; //全ての投稿の情報を入れる

if(file_exists($FILE)) {
    $BOARD = json_decode(file_get_contents($FILE));
}

if($_SERVER['REQUEST_METHOD'] === 'POST'){
    //$_POSTはHTTPリクエストで渡された値を取得する
    //リクエストパラメーターが空でなければ
    if(!empty($_POST['txt'])){
        //投稿ボタンが押された場合
        //$textに送信されたテキストを代入
        $text = $_POST['txt'];
        $tim = $_POST['tim'];
        //新規データ
        $DATA = [$id, $text, $tim];
        $BOARD[] = $DATA;
        $webhook_url = 'XXXX';
        $options = [
            'http' => [
            'method' => 'POST',
            'header' => 'Content-Type: application/json',
            'content' => json_encode([
            'text' => '社長、'.$DATA[2].'時に'.$DATA[1].'の予定が入っています'
            ]),
            ]
        ];
        file_get_contents($webhook_url, false, stream_context_create($options));
        file_put_contents($FILE, json_encode($BOARD));

    }else if(isset($_POST['del'])){
        //削除ボタンが押された場合
        //新しい全体配列を作る
        $NEWBOARD = [];

        foreach($BOARD as $DATA){
            if($DATA[0] !== $_POST['del']){
                $NEWBOARD[] = $DATA;
            }
        }
        //全体配列をファイルに保存する
        file_put_contents($FILE, json_encode($NEWBOARD));
    }
    //Webページを更新)
    header('Location: '.$_SERVER['SCRIPT_NAME']);
    //プログラム終了
    exit;
}
?>

<!DOCTYPE html>
<html lang= "ja">
<head>
    <meta name= "viewport" content= "width=device-width, initial-scale= 1.0">
    <meta http-equiv= "content-type" charset= "utf-8">
    <link rel="stylesheet" type="text/css" href="./todo.css" media="all">
    <title>予定登録</title>
</head>
<body>
    <h1>予定登録</h1>

    <section class= "main">
        <!--投稿-->
        <form method= "post">
            <input type= "text" name= "txt">
            <input type= "time" name= "tim">
            <input type= "submit" value= "投稿">
        </form>    
        <table style= "border-collapse: collapse">
        <!--tableの中でtr部分をループ-->
        <?php foreach((array)$BOARD as $DATA): ?>
        <tr>
        <form method= "post">
            <td>
                <!--テキスト-->
                <?php 
                echo h($DATA[1]." ".$DATA[2]); 
                ?>
            </td>
            <td>
                <!--削除-->
                <input type= "hidden" name= "del" value= "<?php echo $DATA[0]; ?>">
                <input type= "submit" value= "削除">
            </td>
        </form>
        </tr>
        <?php endforeach; ?>
        </table>
    </section>

通知機能

一定時間ごとに通知を行う機能としてPHPのCRONを使用した。

ただ、個人でレンタルしているサーバーの現在のプランでは
10分毎でしか実行できない為、それを踏まえ現時点から15分後までの予定を通知するようにした。

コードは以下の通り

以下のコードをサーバー側のCRONに設定

cron.php
<?php

$FILE = "XXXXXX/todo.txt";
$test = json_decode(file_get_contents($FILE));
$today = date("H:i");

$dateTime1 = date("H:i");
$objDatetime1 = new DateTime($dateTime1);

for ($i = 0; $i < count($test); $i++) {
    echo $test[$i][2]."<br>";
    //echo isset($test[$i][2])."<br>";
    if($test[$i][2]){
        $dateTime2 = $test[$i][2];
        $objDatetime2 = new DateTime($dateTime2);
        $objInterval = $objDatetime1->diff($objDatetime2);
        $sa = $objInterval->format('%H%I');
        $plus = $objInterval->format('%R');
        if ($plus="+"){
            if($sa<=15){
                Teams(sprintf('社長'.$test[$i][1]."の予定まで、あと%02d分です", $sa));
            }
        }
    }else{
        echo "false";
    }
}

function Teams($messege){
    $webhook_url = 'XXXXXXX';
    $options = [
        'http' => [
        'method' => 'POST',
        'header' => 'Content-Type: application/json',
        'content' => json_encode([
        'text' => $messege
        ]),
        ]
    ];
    file_get_contents($webhook_url, false, stream_context_create($options));
}

?>

結果

こうしたら
2020-05-29_09h53_09.png

こうなる

2020-05-29_09h52_44.png

最後に

秘書に冷たい視線を向けられながら
事務的にあれやこれやじゅるじゅるされたい

参考

https://qiita.com/wataash/items/72b49509c3964294dd67
https://qiita.com/masa_mf3qt/items/948a0f4d2857c6739ac5

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPUnitの"expectException()"は、例外を発生させる箇所より前に記載する

環境

$ vendor/bin/phpunit --version
PHPUnit 9.1.5 by Sebastian Bergmann and contributors.

$ php --version
PHP 7.4.0 (cli) (built: May 27 2020 22:16:49) ( NTS )

expectException()を記載したのに、例外を発生させてもテストが成功しなかった

記載する位置、順番を間違えていたのが原因でした。

PHPUnitには、テストコード内で例外が生成されたかを確認するexpectExceptionメソッドが実装されています。
https://phpunit.readthedocs.io/ja/latest/writing-tests-for-phpunit.html#writing-tests-for-phpunit-exceptions

このメソッドは、例外発生箇所より手前で定義する必要があります。
上記公式ドキュメントのサンプルコードを借りて説明すると、

こうではなく、

ExceptionTest.php
<?php
use PHPUnit\Framework\TestCase;

class ExceptionTest extends TestCase
{
    public function testException()
    {
        throw new InvalidArgumentException("ここでわざとエラーを出します。");
        $this->expectException(InvalidArgumentException::class);
    }
}

こうやって変更して

- $this->expectException(InvalidArgumentException::class);
throw new InvalidArgumentException("ここでわざとエラーを出します。");
+ $this->expectException(InvalidArgumentException::class);

こうすべきです。

ExceptionTest.php
<?php
use PHPUnit\Framework\TestCase;

class ExceptionTest extends TestCase
{
    public function testException()
    {
        $this->expectException(InvalidArgumentException::class);
        throw new InvalidArgumentException("ここでわざとエラーを出します。");
    }
}

出力

実行結果です。こちらは、誤った順番で実行した場合

$ vendor/bin/phpunit ./tests/TDDPractice/ExceptionTest.php 
PHPUnit 9.1.5 by Sebastian Bergmann and contributors.

E                                                                   1 / 1 (100%)

Time: 00:00.026, Memory: 4.00 MB

There was 1 error:

1) ExceptionTest::testException
InvalidArgumentException: ここでわざとエラーを出します。

/home/taro/projects/phpsample/tests/TDDPractice/ExceptionTest.php:8

ERRORS!
Tests: 1, Assertions: 0, Errors: 1.

こちらが、正しい順番で実行した場合です。

$ vendor/bin/phpunit ./tests/TDDPractice/ExceptionTest.php 
PHPUnit 9.1.5 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 00:00.045, Memory: 4.00 MB

OK (1 test, 1 assertion)

おわりに

メソッド名をよくよく考えれば、 "assertHoge()" のような名前ではなく、 "expectException" という名前が利用されています。
このニュアンスに気づいていれば、順番をひっくり返すこともなかったかもなと思います。

追記

@expectedExceptionという形式で、テストケースを記載することもできます。
...と書こうと思ったら、手元で動きませんでした。8.x系で非推奨となり、9.x系で削除されたようです。
https://github.com/gordalina/cachetool/issues/141

$this->expectException()と仲良く暮らしていきましょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Lighthouse】schema.graphqlがキャッシュされて更新できない時の対処法【Laravel + GraphQL + Redis】

Lighthouseを使っている時にローカルで実装して動作確認した後、AWS環境(+Redis)にデプロイしてECS確認したらschema.graphqlの更新が全く反映されず、めっちゃハマりました。
この記事ではその解決策と簡単な解説をしていきたいと思います。

結論

.envまたは環境変数にLIGHTHOUSE_CACHE_ENABLE=falseを設定するか、以下のコマンドを実行する。
ただし本番環境で上記の環境変数を設定するのは非推奨なので注意が必要。

php artisan lighthouse:clear-cache

環境

Docker 19.03.8
PHP 7.3.16
Laravel 6.18.3
Lighthouse 4.11

発生したこと

  1. LighthouseResolverを使用してスキーマを実装し、GitHubからAWSのCode兄弟を使ってECSにデプロイ。
  2. ローカルでは元気に動いていたプログラムがECS上では全く動かない。
  3. デバッグログ仕込みまくって調べてみたらschema.graphqlのファイルの更新が反映されてない。
  4. ECSで動いているimageを確認してもちゃんと新しいschema.graphqlが入っている。
  5. キャッシュか?でもデプロイのたびにコンテナ作り直しているからそれはないよなぁ……
  6. 原因がわからなくて詰む。

原因

やっぱりキャッシュでした。

config/lighthouse.phpにキャッシュの設定があります。

config/lighthouse.php
'cache' => [
    'enable' => env('LIGHTHOUSE_CACHE_ENABLE', env('APP_ENV') !== 'local'),
    'key' => env('LIGHTHOUSE_CACHE_KEY', 'lighthouse-schema'),
    'ttl' => env('LIGHTHOUSE_CACHE_TTL', null),
],

enableに書いてある通り、.envAPP_ENVを参照しlocal以外だったら自動でキャッシュを有効化します。

これによりGraphQLの全てのスキーマが一つのデータとしてキャッシュされます。

、このキャッシュはどこに保存されるかというと、メモリではなく.envCACHE_DRIVERで設定されているストレージに保存されます。
(詳細な実装はこちらに書いてあります。)

今回はRedisを使用してたので、そこに保存されることになります。

ローカルならDockerコンテナでRedisを管理しているので、コンテナを落とせば自然とリセットされますがAWSでの開発環境ではRedisを落とすことはほぼありません。

そのため永遠にキャッシュが残ることになってしまったのです。

最後に

注意点としてconfig/lighthouse.phpに書いてある通り、キャッシュをオフにするとパフォーマンスに影響がでます。
なので本番環境ではキャッシュを切らないように気をつけましょう。

/*
|--------------------------------------------------------------------------
| Schema Cache
|--------------------------------------------------------------------------
|
| A large part of schema generation consists of parsing and AST manipulation.
| This operation is very expensive, so it is highly recommended to enable
| caching of the final schema to optimize performance of large schemas.
|
*/

ECSを使用している場合はいちいちコンテナに入るのも大変なので、Dockerfileにコマンドを追加したり直接Redis上のキャッシュを削除するようにしましょう。

参考文献

Schema caching
GraphQL Server Cannot Be Reached Status Code 500
nuwave/lighthouse

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む