20200104のPHPに関する記事は11件です。

プログラミングをかじったからには何らかの制作物を作りたい#3 ~作り直し編、完成したもの~

説明

このエントリーは初心者がとりあえず何かを作りたいと考え、それのみを理由にして記述しているものの3です。そのため、技術的な誤りや勘違いが多分に含まれている可能性があります。ご了承くださいませ。もしよろしければご指摘やご教示を頂けましたら幸いです。

前回のあらすじ

プログラミングをかじったからには何らかの制作物を作りたい# 2
https://qiita.com/tatsuki1112/items/bf363ff7e0a1678eebd6
じゃんけん自体を書いたり、Bootstrapでページを一応レスポンシブ化した。

作り直しとは

じつはこのじゃんけんサイトは最初PythonのFlaskで作成していた。しかしなんとなくやる気が起きずにだいぶ放置していた。その後、PHP、Laravelを使用している会社でインターンを行うこととなったので、勉強をかねてLaravelで作成しようと思い立ち作り直して再開した。

できたもの

このエントリーを行ったときにはこのじゃんけんサイトもうすでに完成していて、やったことの整理という意味で記述しています。
一応の成果物はこちらにあるので、お暇な際にでも遊んでいただけると幸いです。
https://www.mend0.top

実際に使ったもの

Docker 19.03.5
Docker-compose 1.25.0
PHP 7.3.13
Laravel 6.3.0
nginx 1.17
mysql 8.0
jquery
Bootstrap 4

conoha vps 最安プラン
centos 7.7

こんな感じでしょうか

大いに参考にさせていただいたもの

いきなりDockerやLaravelの環境を構築するのは非常に困難なので、以下を参考に環境を整えました。

Laravelの開発環境をDockerを使って構築する
https://qiita.com/ucan-lab/items/17c806973e69792ada99

Laravel 6.0 基本のタスクリスト
https://qiita.com/ucan-lab/items/36f6e89abad26a68f69a

というかほとんどこちらのタスクリストを改変して作成したようなものです。偉大ですね。

あそびかた

それぞれの制作や公開の過程でどのような手順を踏んだかはともかくとして、とりあえずはどんな物ができたのかを御覧いただきたいと思います。

トップページ

スクリーンショット 2020-01-04 23.21.27.png

強い手を探す

指定した回数じゃんけんを行い、そのなかで最も勝数が多かった手を表示するものです。じゃんけんの手を決める自信がないときに利用してください。
スクリーンショット 2020-01-04 23.26.58.png

たとえば99999回じゃんけんを行わせると...
スクリーンショット 2020-01-04 23.28.17.png

このように試行回数などとともに一番つよいじゃんけんの手を教えてくれます。

勝敗を決める

じゃんけんを行う際に、3回勝負などで決着を付ける場合があります。しかしもはや3回では納得ができない場合に利用してください。
スクリーンショット 2020-01-04 23.34.07.png
例えばこの二者にじゃんけんをおこなわせると...

スクリーンショット 2020-01-04 23.35.43.png
こちらが勝利したようです。

回数を見る

こちらの画面ではいままでに行われたじゃんけんの総回数、最大回数、各手の勝利回数を見ることができます。それだけです。
スクリーンショット 2020-01-04 23.39.34.png

レッツじゃんけん!!!!

実際にどのように作成したか、どういったところに苦労したかなどは次回以降記述します。

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

風水プログラミング

今日は趣向を変えて風水のプログラミングをしてみます。

風水?オカルト?
「そんなの風水師が適当なこと言ってるんじゃないの?」
と思う人もいるかもしれません。

いいえ、そんなことはありません。
風水、九星気学、四柱推命は計算のアルゴリズムがしっかりと確立していて、入力が同じならば出力は同じになるという特徴を持っています。

成立した経緯はわかりませんが、昔の中国の人がたくさんの運命を見てきて統計的な法則を見つけたけどわかりにくいから陰陽五行にあてはめてみたとかそういう類じゃないかと思います。

例えば、風水ならば、竣工年と建物の向きが決まれば、結果は一意に定まります。
四柱推命も誕生の年月日と時間で結果は一意に決まります。

入力が決まれば出力も一意に決まる っていいですね。

そのアルゴリズムはちゃんとした本を読めば書いてあります。陰陽五行の話とかいろいろ出てくるのですが、結局のところは、

  • 足し算や引き算をする
  • テーブルを引く
  • 割り算して余りを求める
  • if文で分類して、パターンを減らす

ということを繰り返しています。 

ロジカルで、まさにプログラミングにぴったりですね!

作ったプログラムの一部を示しましょう。
まずは基本のチャートを描くためのもの。

    $a = array();
    $a[0] = $basenum + 8;
    $a[1] = $basenum + 4;
    $a[2] = $basenum + 6;
    $a[3] = $basenum + 7;
    $a[4] = $basenum + 0;
    $a[5] = $basenum + 2;
    $a[6] = $basenum + 3;
    $a[7] = $basenum + 5;
    $a[8] = $basenum + 1;
    $text = "";
    for($i = 0; $i < 9;$i++ )
    {
        $text .= strval(($a[$i] - 1) % 9 + 1);
        if($i != 8) $text .= ",";
    }

こういうチャートを描くためのものです。
image.png

次は、運星から山星・水星を求めるプログラム。ちょっと複雑になってくるので変数名がaとかだと破綻します。

    $運星 = array(); // 
    $運星["東南"] = 剰余($era + 8); // 
    $運星["南"  ] = 剰余($era + 4); // 
    $運星["南西"] = 剰余($era + 6); // 
    $運星["東"  ] = 剰余($era + 7); // 
    $運星["中央"] = 剰余($era + 0); // 
    $運星["西"]   = 剰余($era + 2); // 
    $運星["北東"] = 剰余($era + 3); // 
    $運星["北"]   = 剰余($era + 5); // 
    $運星["北西"] = 剰余($era + 1); // 

    switch($向) {
        case "N1": case "N2": case "N3":
            $中央水星 = $運星["北"];
            $中央山星 = $運星["南"];
            break;
        case "NE1": case "NE2": case "NE3":
            $中央水星 = $運星["北東"];
            $中央山星 = $運星["南西"];
            break;
        case "E1": case "E2": case "E3":
            $中央水星 = $運星["西"];
            $中央山星 = $運星["東"];
            break;
        case "SE1": case "SE2": case "SE3":
            $中央水星 = $運星["東南"];
・・・(中略)・・・
        case "NW1": case "NW2": case "NW3":
            $中央水星 = $運星["北西"];
            $中央山星 = $運星["東南"];
            break;
    }

    $山星 = array();
    if(  (preg_match("/[23]/",$向) && !($中央山星 % 2))    // 偶数で区分2か3
      || (preg_match("/[1]/",$向)  &&  ($中央山星 % 2))) { // 奇数で区分1
      $山星順行 = true;
    }
    else {
      $山星順行 = false;
    }

    $水星 = array();
    if(  (preg_match("/[23]/",$向) && !($中央水星 % 2))    // 偶数で区分2か3
      || (preg_match("/[1]/",$向)  &&  ($中央水星 % 2))) { // 奇数で区分1
      $水星順行 = true;
・・・(中略)・・・
    if($中央山星 == 5) { // 5は特別扱い
        if(  (preg_match("/[23]/",$向) && !($era % 2))    // 偶数で区分2か3
          || (preg_match("/[1]/",$向)  &&  ($era % 2))) { // 奇数で区分1
          $山星順行 = true;
        }
        else {
          $山星順行 = false;
        }
    }

怪しげな変数名が並んでいます。

PHPは変数名に普通に漢字が使えるし、連想配列が使えるのでわかりやすく書けました。
もし、英数字しか使えないとどうやって英語に訳せばよいかわからなくて困っていたことでしょう。
 
こういう計算を延々とやっていくと、下の図のようなチャートが作られます。
image.png

こうして年飛星まで入ったチャートをPNGファイルで出力するPHPのプログラムが出来ました。

この数字を求めるところまでは機械的にできるのですが、数字を解釈するところに風水師の経験や勘といったものが必要になります。もちろん、私にはできません。

ある程度の一般的な解釈まではプログラムでもできるかもしれませんが、最終的には人間がいるのでしょう。

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

PHP7.4をCentOS8にインストール

はじめに

お正月を使って自宅色々兼用サーバを役割ごとに仮想化させました。
仮想化ベースはCentOS8/KVMで、本稼働VM部分とは別に検証し放題のテストVM環境も作ったので今後色々遊べそうです。
サーバ構築部分はいつか別途書こうと思います。
ちなみに現状VM4インスタンス同時起動でも、ほとんどの時間がアイドル状態なので35w程度の消費電力です。

んで、PHP7.4.1が安定版になってるのでインストール用メモ。
例のごとくRemi、epelリポジトリからダウンロードします。

インストール

リポジトリのインストール

# epel
$ sudo dnf -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm
# -略-
# remi
$ sudo dnf -y install https://rpms.remirepo.net/enterprise/remi-release-8.rpm
# -略-

PHP7.4.1のインストール

インストール可能なphpモジュールを確認。

$ sudo dnf module list php
# -略-

リストで確認できたremi-7.4を選択

$ sudo dnf module enable php:remi-7.4

=========
パッケージ    アーキテクチャー     バージョン     リポジトリ      サイズ
=========
Enabling module streams:
 php                                     remi-7.4

必要なパッケージをインストール

$ sudo dnf install php php-cli php-common
# -略-

インストール確認

$ php -v

PHP 7.4.1 (cli) (built: Dec 17 2019 16:35:58) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
    with Zend OPcache v7.4.1, Copyright (c), by Zend Technologies

以上です。

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

Laravelでよく使うコマンド、記法

新規プロジェクト作成

$ laravel new (アプリ名)

または

$ composer create-project laravel/laravel (アプリ名) --prefer-dist

artisanコマンド

コントローラの作成

$ php artisan make:controller (コントローラ名)

マイグレーションの作成

$ php artisan make:migration create_(テーブル名)_table

モデルの作成

$ php artisan make:model (モデル名)

シーダーの作成

$ php artisan make:seeder (テーブル名)TableSeeder

マイグレーションとシーダーの反映

$ php artisan migrate --seed

データベースのリフレッシュ

$ php artisan migrate:reset

web.php記法

getメソッド

routes/web.php
Route::get('(相対パス)', function () {
    // 処理
});
routes/web.php
Route::get('(相対パス)', 'コントローラ名@関数')->name('名前');

postメソッド

routes/web.php
Route::post('(相対パス)', 'コントローラ名@関数')->name('名前');

URLに変数を混ぜる

routes/web.php
Route::get('/hoge/{id}', 'コントローラ名@関数')->name('名前');
routes/web.php
Route::get('/hoge/{id?}', 'コントローラ名@関数')->name('名前');

マイグレーション記法

Schema::table('users', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('name');
    $table->string('email')->unique();
    $table->timestamp('email_verified_at')->nullable();
});

カラムタイプ

コマンド 説明
$table->bigIncrements('id'); BIGINT
$table->integer('votes'); integer(整数)
$table->string('name', 100); string(文字列)
$table->float('amount', 8, 2); float(実数)
$table->boolean('confirmed'); boolean(真偽)
$table->date('created_at'); date(日付)
$table->timestamp('added_on'); timestamp(時間)

カラム修飾子

コマンド 説明
->unique() uniqueキー追加
->nullable() NULL値を許容

モデル記法

fillable

create()やupdate()を受け付けるホワイトリスト

protected $fillable = [
    'name', 'email', 'password'
];

guarded

create()やupdate()を受け付けないブラックリスト

protected $guarded = [
     'create_at', 'update_at'
];

hidden

パスワードなどの秘匿性の高いカラム

protected $hidden = [
     'password', 'remember_token'
];

シーダー記法

database/seeds/DatabaseSeeder.php
<?php

use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        Model::unguard();

        $this->call('Seeder名');

        Model::reguard();
    }
}
<?php

use Illuminate\Database\Seeder;

use App\モデル名;

class MemoTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        DB::table('テーブル名')->delete(); //最初に全件削除

        モデル名::create([
            'name' => 'aaa', 'email' => 'aaa@example.com'
        ]);
    }
}

データの取得

全件取得

$items = Item::get();

最初の要素だけ取得

$item = Item::first();

id指定で取得

idというカラムがある場合のみ使用可能

$id = 1;
$item = Item::find($id); // 配列ではなく要素
$items = Item::find([1, 3]); // 長さ2の配列

条件付きでデータを取得

nameというカラムの値が「名前1」というデータを全件取得する。

$items = Item::where('name', '名前1')->get();

AND/OR検索

AND検索

15歳の鈴木さんのデータのみ取得する。

$items = Item::where('age', 15)
    ->where('name', '鈴木')
    ->get();
$items = Item::where([
    ['age', 15],
    ['name', '鈴木']
])->get();
OR検索

鈴木さんと山田さんのデータを取得する。

$items = Item::where('name', '鈴木')
    ->orWhere('name', '山田')
    ->get();

特定の文字列を含んだデータの取得

nameに「川」という文字列を含むデータを全件取得する。

$items = Item::where('name', 'like', '%川%')->get();

比較によるデータの取得

idが5よりも大きいデータを全件取得する。

$items = Item::where('id', '>', 5)->get();

idが5以下のデータを全件取得する。

$items = Item::where('id', '<=', 5)->get();

時間情報で検索

日付で検索
$items = Item::whereDate('created_at', '2020-01-04')->get();
年で検索
$items = Item::whereYear('created_at', '2020')->get();
月で検索
$items = Item::whereDate('created_at', '1')->get();
日で検索
$items = Item::whereDay('created_at', '4')->get();

並び替えて取得

昇順 (1, 2, 3, 4)
$items = Item::orderBy('id', 'asc')->get();
降順 (4, 3, 2, 1)
$items = Item::orderBy('id', 'desc')->get();

集約したデータの取得

件数の取得
$count = Item::count();
最大値の取得

idが一番大きいデータを取得

$item = Item::max('id');

データの編集や削除

データの更新

nameを鈴木に変更する

$item = Item::first();
$item->update([
    'name' => '鈴木'
]);

データの追加

25歳の鈴木さんのデータを追加する

Item::create([
    'name' => '鈴木',
    'age' => 25
]);

データの削除

全件削除
Item::get()->delete();
特定のデータのみ削除
Item::where('id', 2)->delete();
id指定で削除

idというカラムがある場合のみ使用可能

$id = 1;
Item::destroy($id);

複数削除できる

Item::destroy([1, 3]);

コントローラ記法

class コントローラ名 extends Controller
{
    public function 関数名()
    {
        // 処理
    }
}

web.phpでURLに変数を含めた場合、引数としてその変数を受け取ることができる。

class コントローラ名 extends Controller
{
    public function 関数名($変数名)
    {
        // 処理
    }
}

Viewの表示

Viewの名前はresources/views/からのパスを記述する。

return view("Viewの名前");

Viewにデータを渡す。

return view("Viewの名前", ['item' => $item]);

リダイレクト

web.phpのnameメソッドで指定した名前を入れる。

return redirect()->route('名前');

URLに変数がある場合

return redirect()->route('名前', ['item' => $item]);

formで送信されたデータの処理

$request->input()で取得可能

public function 関数名(Request $request)
{
     $name = $request->input('name');
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Uncaught Error: Call to undefined function~が出た時は

前回のあらすじ

アンケートシステム作ってるなう
・タグでいたずらされないようにしてた。

今日やったこと(ハイライト)

・XAMPPでMySQLのDBを構築した
・DBにアンケートデータが自動で保存されるよう頑張った
(今回の教科書『いきなりはじめるPHP ワクワク・ドキドキの入門教室』)

困ったこと

・Uncaught Error: Call to undefined function mb_internal_encording()~~って名前のエラーが出た

原因

mb_internal_enco「 r 」ding スペルミスってた!!
mb_internal_encoding  直した!!やったね!!

感想

自動保存の機能を追加するためのソースを書いてる上で、文字列の連結を理解するのが難しかった。てことで、明日以降の私に丸投げした。

次回の目標

アンケートシステムをもっと便利にするよう学習していくぞい

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

Atom の PHP 補完を vim-lsp で使う

Vim で PHP を書くときは vim-lsp で Intelephense を使っているのですが、プロプライエタリなソフトウェアなのでサーバサイドのコードが公開されていません。Intelephense のウェブサイトはあまり親切ではなく、初めてインストールしたときは正しく動かせているのかよく分からなかった事を覚えています。
そのため OSS の PHP LSP を探していたところ Serenata というソフトウェアを見つけました。

Serenata は Atom エディタのプラグインとして公開されていたようです。サーバ型の Serenata 本体と、Atom プラグインの php-ide-serenata の2つで構成されています。バージョン4までは Atom エディタ用ですがバージョン5で Language Server Protocol をサポートしたようです。
対応している LSP の機能は Language Server Protocol Support Table のページ で確認できます。

php-ide-serenataの動作画面

php-ide-serenata.png

今回はこの Serenata をvim-lspから使えるように設定してみました。

vim-lsp の動作画面
serenata_completion.png

Serenata はTCPソケットしかサポートされていなかったので、vim-lsp の標準入出力とつなげるために socat コマンドを使いました。
ひとまず vim 側での補完やホバー表示ができるところまでは確認したのですが、速度面で難があったり、vim-lsp の起動initialize/終了exitとうまく連動できないので実用レベルには至りませんでした。 もう少し調査して使えるようにしたいのですが一旦ここまでの記録を公開します。

事前準備

vim-lsp が動く状態になっていることが前提です。その他に必要なソフトウェアは以下の通りです。

  • PHP 7.1+
    • mbstring
    • xml
    • libxml
    • dom
    • openssl
    • pdo_sqlite
  • socat コマンド
  • vim
    • vim-lsp

socat コマンドがインストールされているか確認してください。macOS は brew でインストール可能です。

$ which socat
/usr/local/bin/socat
$ socat -V
socat by Gerhard Rieger and contributors - see www.dest-unreach.org
socat version 1.7.3.3 on May 11 2019 02:21:21
...
$ brew install socat # インストールされていなければ

Serenata の導入

1. 実行可能な PHAR 形式のファイルをダウンロード

Serenata サーバをダウンロードしてください。インストール方法はいくつかありますが実行可能な PHAR 形式のファイルが簡単です。PHAR compatible with PHP 7.x を選択してください。

2. ポート 11111 で起動

Serenata サーバをポート 11111 で起動してみましょう。Serenata はソースコードの解析結果をデータベースに保存します。デフォルト設定では SQLite をインメモリで使用します。メモリ使用量が多いので php の起動オプションでメモリを多めに確保します。

$ php -d memory_limit=2048M ~/bin/serenata.phar --uri tcp://127.0.0.1:11111
Starting server bound to socket on URI tcp://127.0.0.1:11111...

正常に起動できれば Starting server bound .... と表示されて LISTEN 状態になります。

3. 動作確認

動作確認のために initialize メソッドを実行してみます。socat を使って Serenata サーバのポート 11111 に送信します。Content-Length には JSON のバイト数が書かれていますので、rootPath rootUri を変更したら Content-Length も忘れずに変更してください。1バイトでも合わないと正しく動作しません。
また、保存するファイルのフォーマットにも注意してください。LSP の仕様で改行コードは CRLF 、ファイル最終行の行末には改行を含めないようにしてください。

serenata.jsonrpc
Content-Length: 1429

{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":19002,"rootPath":"/Users/foobar/src/foobar/foobarbazfo/","rootUri":"file:///Users/foobar/src/foobar/foobarbazfo/","workspaceFolders":[],"capabilities":{"workspace":{"applyEdit":true,"configuration":false,"workspaceEdit":{"documentChanges":true},"workspaceFolders":false,"didChangeConfiguration":{"dynamicRegistration":false},"didChangeWatchedFiles":{"dynamicRegistration":false},"symbol":{"dynamicRegistration":false},"executeCommand":{"dynamicRegistration":false}},"textDocument":{"synchronization":{"dynamicRegistration":false,"willSave":true,"willSaveWaitUntil":true,"didSave":true},"completion":{"dynamicRegistration":false,"completionItem":{"snippetSupport":true,"commitCharactersSupport":false},"contextSupport":true},"hover":{"dynamicRegistration":false},"signatureHelp":{"dynamicRegistration":false},"references":{"dynamicRegistration":false},"documentHighlight":{"dynamicRegistration":false},"documentSymbol":{"dynamicRegistration":false,"hierarchicalDocumentSymbolSupport":true},"formatting":{"dynamicRegistration":false},"rangeFormatting":{"dynamicRegistration":false},"onTypeFormatting":{"dynamicRegistration":false},"definition":{"dynamicRegistration":false},"codeAction":{"dynamicRegistration":false},"codeLens":{"dynamicRegistration":false},"documentLink":{"dynamicRegistration":false},"rename":{"dynamicRegistration":false}},"experimental":{}}}}

動作確認するだけならパスの変更は不要です

実行して Content-Length で始まるレスポンスが返ってくれば成功です。

$ socat stdio tcp4:127.0.0.1:11111,shut-none < serenata.jsonrpc
Content-Length: 951

{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"textDocumentSync":{"openClose":false,"change":1,"willSave":false,"willSaveWaitUntil":false,"save":{"includeText":true}},"hoverProvider":true,"completionProvider":{"resolveProvider":false,"triggerCharacters":null},"signatureHelpProvider":{"triggerCharacters":["(",","]},"definitionProvider":true,"typeDefinitionProvider":false,"implementationProvider":false,"referencesProvider":false,"documentHighlightProvider":true,"documentSymbolProvider":true,"workspaceSymbolProvider":false,"codeActionProvider":false,"codeLensProvider":{"resolveProvider":true},"documentFormattingProvider":false,"documentRangeFormattingProvider":false,"documentOnTypeFormattingProvider":null,"renameProvider":false,"documentLinkProvider":null,"colorProvider":false,"foldingRangeProvider":false,"executeCommandProvider":null,"workspace":{"workspaceFolders":{"supported":false,"changeNotifications":false}},"experimental":null}}}

vim-lsp 設定

1. lsp#register_server の設定

Serenata が正しく動いていることが確認できたので vim-lsp の設定を追加してください。

au User lsp_setup call lsp#register_server({
    \ 'name': 'serenata',
    \ 'cmd': {server_info->['socat', 'stdio', 'tcp4:127.0.0.1:11111,shut-none']},
    \ 'initialization_options': {"rootPath":"/Users/foobar/src/foobar/foobarbazfo/","rootUri":"file:///Users/foobar/src/foobar/foobarbazfo/"},
    \ 'whitelist': ['php'],
    \ 'workspace_config': { 'serenate': {
    \   'files.associations': ['*.php'],
    \ }},
    \ })

2. 動作確認

Serenataサーバを起動した状態で vim で PHP のファイルを開きます。vim-lsp が Serenata サーバに正しく接続できれば :LspStatus の実行結果が serenata: running になります。vim-lsp と Serenata の起動・終了の仕様に齟齬があるので、vim を終了した場合は Serenata サーバも再起動しないと LspStatus の結果が failed になります。

まとめ

Serenata はサーバ型なので LSP の exit メソッド実行されても終了せず、vim-lsp が再度 initialize メソッドを実行するとエラーになります。速度も Atom エディタで使うより遅いのでもう少し改善の余地がありそうです。
また成果があれば公開します。

その他のスクリーンショット

クラス名の補完
serenata_completion_type.png
メソッド補完
selenata_completion_method.png
クラスのホバー表示
serenata_hover_type.png
メソッドのホバー表示
serenata_hover_method.png

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

Laravelで簡単なメモアプリを作る(3)~データの更新と削除~

はじめに

Laravelとは、Webに特化したPHPフレームワーク。セキュリティ対策(CSRF対策など)もこれでできます。MVCアーキテクチャを採用しています。今回は、データの更新と削除をしていきたいと思います。

Laravelで簡単なメモアプリを作る(1)~Viewの作成と表示~
Laravelで簡単なメモアプリを作る(2)~データの取得と保存~

今回の目標物

スクリーンショット 2019-12-31 11.25.14.png

スクリーンショット 2019-12-31 11.25.26.png

データの更新

更新するデータの取得

id=1のデータを取得する時は、このように書く。

$memo = Memo::find(1);

whereメソッドでも、取得することができる。ただ、whereメソッドは、findメソッドと異なって、条件に合うものを全て取ってくるため、たとえ条件に合うものが一つであったとしても配列として取得するので注意。なのでfirstメソッドで、要素として取得する必要がある。

$memo = Memo::where('id', 1)->first();

データの更新

updateメソッドで、データベース上のデータを書き換えることができる。

$memo->update([
    'title' => $title,
    'content' => $content
]);

MemoControllerを以下のように編集する。

app/Http/Controllers/MemoController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Memo;

class MemoController extends Controller
{
    public function showHome()
    {
        $memos = Memo::get();
        return view("home", ['memos' => $memos]);
    }

    public function showSubmit($id = 0)
    {
        if ($id != 0) {
            $memo = Memo::where('id', $id)->get()->first();
        } else {
            $memo = (object) ["id" => 0, "title" => "", "content" => ""];
        }
        return view("submit", ['memo' => $memo]);
    }

    public function postSubmit(Request $request, $id = 0)
    {
        $title = $request->input('title');
        $content = $request->input('content');
        if ($id == 0) {
            Memo::create([
                'title' => $title,
                'content' => $content
            ]);
        } else {
            $memo = Memo::find($id);
            $memo->update([
                'title' => $title,
                'content' => $content
            ]);
        }
        return redirect()->route('home');
    }
}

データの削除

メモを削除する関数の作成

destroyメソッドを使うと、引数に指定したidのデータだけが削除される。下のコードをMemoControllerに追加する。

public function deleteMemo($id)
{
    Memo::destroy($id);
    return redirect()->route('home');
}

web.phpにdeleteMemoメソッドの登録

/delete/{id}というURLを受け取ったら、メモが削除されるようにする。下のコードをweb.phpに追加する。

Route::get('/delete/{id}', 'MemoController@deleteMemo')->name('delete');

削除ボタンを押したら削除されるようにする

resources/views/home.blade.phpの削除ボタンを以下のように修正する。

<td><a href="{{ route('delete', ['id' => $memo->id])}}">削除</a></td>

おまけ

モーダルを使って、削除の確認をする

bootstrap4のモーダルを使用する。
https://getbootstrap.com/docs/4.3/components/modal/

resources/views/home.blade.php
@extends('layouts.app')

@section('css')
<style>
    header {
        height: 50px;
        background-color: #000;
        color: white;
        padding-left: 20px;
        font-size: large;
        color: #ddd;
    }

    .title {
        position: absolute;
        top: 10px;
    }

    .card {
        margin-top: 40px;
    }

    .left {
        width: 70%;
    }

    .submit {
        position: absolute;
        top: 10px;
        right: 20px;
    }
</style>
@endsection

@section('content')
<div class="card" style="width: 100%;">
    <div class="card-header">
        メモ一覧
        <a href="{{ route('submit')}}" class="submit">メモを追加</a>
    </div>

    <table class="table">
        <tbody>
            @foreach ($memos as $memo)
            <tr>
                <td class="left">{{$memo->title}}</td>
                <td><a href="{{ route('submit', ['id' => $memo->id])}}">編集</a></td>
                <td><a type="button" data-toggle="modal" data-target="#modal{{$memo->id}}">削除</a></td>
            </tr>
            <!-- Modal -->
            <div class="modal fade" id="modal{{$memo->id}}" tabindex="-1" role="dialog" aria-hidden="true">
                <div class="modal-dialog" role="document">
                    <div class="modal-content">
                        <div class="modal-header">
                            <h5 class="modal-title">以下のメモを本当に削除しますか?</h5>
                            <button type="button" class="close" data-dismiss="modal">
                                <span aria-hidden="true">&times;</span>
                            </button>
                        </div>
                        <div class="modal-body">
                            {{$memo->title}}
                        </div>
                        <div class="modal-footer">
                            <button type="button" class="btn btn-secondary" data-dismiss="modal">閉じる</button>
                            <button type="button" class="btn btn-primary"
                                onclick="location.href='{{ route('delete', ['id' => $memo->id])}}'">
                                削除
                            </button>
                        </div>
                    </div>
                </div>
            </div>
            @endforeach
        </tbody>
    </table>
</div>
@endsection

submitページのボタンを新規作成の時と編集の時で変える

新規作成の時は$memo->id == 0で、編集の時は$memo->id != 0なので、if文で条件分岐する。
resources/views/submit.blade.phpの追加ボタンを以下のように書き換える。

@if($memo->id == 0)
<button type="submit" class="btn btn-success">追加</button>
@else
<button type="submit" class="btn btn-success">適用</button>
@endif

テーブルの列をクリックしたら、詳細画面に飛べるようにする

下のコードを、app.blade.phpのbodyタグの一番下に追加する。

resources/views/layouts/app.blade.php
<script>
    jQuery(function($) {
                $('tbody tr[data-href]').addClass('clickable').click(function() {
                    window.location = $(this).attr('data-href');
                }).find('a').hover(function() {
                    $(this).parents('tr').unbind('click');
                }, function() {
                    $(this).parents('tr').click( function() {
                        window.location = $(this).attr('data-href');
                    });
                });
            });
</script>

home.blade.phpの編集ボタンを消して、trタグにdata-href属性を追加する。

resources/views/home.blade.php
<tr data-href="{{route('submit', ['id' => $memo->id])}}">
       <td class="left">{{$memo->title}}</td>
       <td><a type="button" data-toggle="modal" data-target="#modal{{$memo->id}}">削除</a></td>
</tr>

テーブルの列をホバーすると色が変わるようにする

trタグにclass="table-row"を追加する。

resources/views/home.blade.php
<tr data-href="{{route('submit', ['id' => $memo->id])}}" class="table-row">
       <td class="left">{{$memo->title}}</td>
       <td><a type="button" data-toggle="modal" data-target="#modal{{$memo->id}}">削除</a></td>
</tr>
resources/views/home.blade.php
.table-row {
    background-color: #fff;
}

.table-row:hover {
    background-color: #ddd;
}

追加ボタンをいい感じにする。ヘッダーも固定する。

app.blade.phpでFont Awesomeを読み込む。

resources/views/layouts/app.blade.php
<link href="https://use.fontawesome.com/releases/v5.6.1/css/all.css" rel="stylesheet">

app.blade.phpを以下のように編集する。

resources/views/layouts/app.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>MemoApp</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.0/css/bootstrap.min.css"
        integrity="sha384-SI27wrMjH3ZZ89r4o+fGIJtnzkAnFs3E4qz9DIYioCQ5l9Rd/7UAa8DHcaL8jkWt" crossorigin="anonymous">
    <link href="https://use.fontawesome.com/releases/v5.6.1/css/all.css" rel="stylesheet">
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
        integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous">
    </script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
        integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous">
    </script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.0/js/bootstrap.min.js"
        integrity="sha384-3qaqj0lc6sV/qpzrc1N5DC6i1VRn/HyX4qdPaiEFbn54VjQBEU341pvjz7Dv3n6P" crossorigin="anonymous">
    </script>
</head>

<style>
    header {
        background-color: #000;
        background-position: 0px 50px;
        padding-left: 20px;
        font-size: large;
        width: 100%;
        height: 50px;
        clear: both;
        position: fixed;
        top: 0;
        margin-top: 0px;
        z-index: 1;
    }

    .title {
        position: absolute;
        top: 10px;
        color: #fff;
    }

    .container {
        margin-top: 70px;
    }
</style>

@yield('css')

<body>
    <header>
        <span class="title">MemoApp</span>
    </header>

    <div class="container">
        @yield('content')
    </div>

    <script>
        jQuery(function($) {
                $('tbody tr[data-href]').addClass('clickable').click(function() {
                    window.location = $(this).attr('data-href');
                }).find('a').hover(function() {
                    $(this).parents('tr').unbind('click');
                }, function() {
                    $(this).parents('tr').click( function() {
                        window.location = $(this).attr('data-href');
                    });
                });
            });
    </script>
</body>

</html>

home.blade.phpを以下のように編集する。

resources/views/home.blade.php
@extends('layouts.app')

@section('css')
<style>
    .table-row {
        background-color: #fff;
    }

    .table-row:hover {
        background-color: #ddd;
    }

    .card {
        margin-top: 40px;
    }

    .circle-button {
        position: fixed;
        display: inline-block;
        text-decoration: none;
        color: rgba(152, 152, 152, 0.43);
        width: 80px;
        height: 80px;
        line-height: 80px;
        font-size: 40px;
        border-radius: 50%;
        text-align: center;
        overflow: hidden;
        font-weight: bold;
        background-image: linear-gradient(#e8e8e8 0%, #d6d6d6 100%);
        text-shadow: 1px 1px 1px rgba(255, 255, 255, 0.66);
        box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.5), 0 2px 2px rgba(0, 0, 0, 0.19);
        border-bottom: solid 2px #b5b5b5;
    }

    .circle-button i {
        line-height: 80px;
    }

    .circle-button:active {
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5), 0 2px 2px rgba(0, 0, 0, 0.19);
        border-bottom: none;
    }

    @media screen and (min-width:0px) {
        .circle-button {
            bottom: 20px;
            right: 30px;
        }
    }

    @media screen and (min-width:768px) and (max-width:1024px) {
        .circle-button {
            bottom: 20px;
            right: 50px;
        }
    }

    @media screen and (min-width:1024px) {
        .circle-button {
            bottom: 30px;
            right: 150px;
        }
    }

    .delete-button {
        display: inline-block;
        text-decoration: none;
        width: 20px;
        height: 20px;
        line-height: 20px;
        font-size: 7px;
        border-radius: 50%;
        text-align: center;
        overflow: hidden;
        background-color: #cd5c5c;
        margin-right: 10px;
        margin-top: 5px;
    }

    .delete-button i {
        line-height: 20px;
        color: #fff;
    }

    .td-left {
        width: 10px;
    }
</style>
@endsection

@section('content')
<div class="card" style="width: 100%;">
    <div class="card-header">
        メモ一覧
    </div>

    <table class="table">
        <tbody>
            @foreach ($memos as $memo)
            <tr data-href="{{route('submit', ['id' => $memo->id])}}" class="table-row">
                <td class="td-left">
                    <a class="delete-button" type="button" data-toggle="modal" data-target="#modal{{$memo->id}}">
                        <i class="fa fa-minus"></i>
                    </a>
                </td>
                <td class="td-right">
                    {{$memo->title}}
                </td>
            </tr>
            <!-- Modal -->
            <div class="modal fade" id="modal{{$memo->id}}" tabindex="-1" role="dialog" aria-hidden="true">
                <div class="modal-dialog" role="document">
                    <div class="modal-content">
                        <div class="modal-header">
                            <h5 class="modal-title">以下のメモを本当に削除しますか?</h5>
                            <button type="button" class="close" data-dismiss="modal">
                                <span aria-hidden="true">&times;</span>
                            </button>
                        </div>
                        <div class="modal-body">
                            {{$memo->title}}
                        </div>
                        <div class="modal-footer">
                            <button type="button" class="btn btn-secondary" data-dismiss="modal">閉じる</button>
                            <button type="button" class="btn btn-primary"
                                onclick="location.href='{{ route('delete', ['id' => $memo->id])}}'">
                                削除
                            </button>
                        </div>
                    </div>
                </div>
            </div>
            @endforeach
        </tbody>
    </table>
</div>

<a href="{{route('submit')}}" class="circle-button">
    <i class="fa fa-plus"></i>
</a>

@endsection

submit.blade.phpのcss部分を消しておく。inputもついでに文字数制限をかける。

resources/views/submit.blade.php
@extends('layouts.app')

@section('content')
<form method="POST" action="{{ route('submit', ['id' => $memo->id])}}">
    @csrf
    <div class="form-group">
        <label for="title">タイトル</label>
        <input type="text" class="form-control" id="title" name="title" value="{{$memo->title}}" maxlength="24"
            required>
    </div>
    <div class="form-group">
        <label for="content">内容</label>
        <input type="text" class="form-control" id="content" name="content" value="{{$memo->content}}" maxlength="60"
            required>
    </div>
    <a href="{{ route('home')}}" class="btn btn-primary">戻る</a>
    @if($memo->id == 0)
    <button type="submit" class="btn btn-success">追加</button>
    @else
    <button type="submit" class="btn btn-success">適用</button>
    @endif
</form>
@endsection

スクリーンショット 2020-01-04 23.29.44.png

今回はここまで。次回は、ログイン機能を実装します。

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

Laravelで簡単なメモアプリを作る(3)~データの編集と削除~

はじめに

Laravelとは、Webに特化したPHPフレームワーク。セキュリティ対策(CSRF対策など)もこれでできます。MVCアーキテクチャを採用しています。今回は、データの編集と削除をしていきたいと思います。

Laravelで簡単なメモアプリを作る(1)~Viewの作成と表示~
Laravelで簡単なメモアプリを作る(2)~データの取得と保存~

今回の目標物

スクリーンショット 2019-12-31 11.25.14.png

スクリーンショット 2019-12-31 11.25.26.png

データの編集

編集するデータの取得

id=1のデータを取得する時は、このように書く。

$memo = Memo::find(1);

whereメソッドでも、取得することができる。ただ、whereメソッドは、findメソッドと異なって、条件に合うものを全て取ってくるため、たとえ条件に合うものが一つであったとしても配列として取得するので注意。なのでfirstメソッドで、要素として取得する必要がある。

$memo = Memo::where('id', 1)->first();

データの編集

updateメソッドで、データベース上のデータを書き換えることができる。

$memo->update([
    'title' => $title,
    'content' => $content
]);

MemoControllerを以下のように編集する。

app/Http/Controllers/MemoController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Memo;

class MemoController extends Controller
{
    public function showHome()
    {
        $memos = Memo::get();
        return view("home", ['memos' => $memos]);
    }

    public function showSubmit($id = 0)
    {
        if ($id != 0) {
            $memo = Memo::where('id', $id)->get()->first();
        } else {
            $memo = (object) ["id" => 0, "title" => "", "content" => ""];
        }
        return view("submit", ['memo' => $memo]);
    }

    public function postSubmit(Request $request, $id = 0)
    {
        $title = $request->input('title');
        $content = $request->input('content');
        if ($id == 0) {
            Memo::create([
                'title' => $title,
                'content' => $content
            ]);
        } else {
            $memo = Memo::find($id);
            $memo->update([
                'title' => $title,
                'content' => $content
            ]);
        }
        return redirect()->route('home');
    }
}

データの削除

メモを削除する関数の作成

destroyメソッドを使うと、引数に指定したidのデータだけが削除される。下のコードをMemoControllerに追加する。

public function deleteMemo($id)
{
    Memo::destroy($id);
    return redirect()->route('home');
}

web.phpにdeleteMemoメソッドの登録

/delete/{id}というURLを受け取ったら、メモが削除されるようにする。下のコードをweb.phpに追加する。

Route::get('/delete/{id}', 'MemoController@deleteMemo')->name('delete');

削除ボタンを押したら削除されるようにする

resources/views/home.blade.phpの削除ボタンを以下のように修正する。

<td><a href="{{ route('delete', ['id' => $memo->id])}}">削除</a></td>

おまけ

モーダルを使って、削除の確認をする

bootstrap4のモーダルを使用する。
https://getbootstrap.com/docs/4.3/components/modal/

resources/views/home.blade.php
@extends('layouts.app')

@section('css')
<style>
    header {
        height: 50px;
        background-color: #000;
        color: white;
        padding-left: 20px;
        font-size: large;
        color: #ddd;
    }

    .title {
        position: absolute;
        top: 10px;
    }

    .card {
        margin-top: 40px;
    }

    .left {
        width: 70%;
    }

    .submit {
        position: absolute;
        top: 10px;
        right: 20px;
    }
</style>
@endsection

@section('content')
<div class="card" style="width: 100%;">
    <div class="card-header">
        メモ一覧
        <a href="{{ route('submit')}}" class="submit">メモを追加</a>
    </div>

    <table class="table">
        <tbody>
            @foreach ($memos as $memo)
            <tr>
                <td class="left">{{$memo->title}}</td>
                <td><a href="{{ route('submit', ['id' => $memo->id])}}">編集</a></td>
                <td><a type="button" data-toggle="modal" data-target="#modal{{$memo->id}}">削除</a></td>
            </tr>
            <!-- Modal -->
            <div class="modal fade" id="modal{{$memo->id}}" tabindex="-1" role="dialog" aria-hidden="true">
                <div class="modal-dialog" role="document">
                    <div class="modal-content">
                        <div class="modal-header">
                            <h5 class="modal-title">以下のメモを本当に削除しますか?</h5>
                            <button type="button" class="close" data-dismiss="modal">
                                <span aria-hidden="true">&times;</span>
                            </button>
                        </div>
                        <div class="modal-body">
                            {{$memo->title}}
                        </div>
                        <div class="modal-footer">
                            <button type="button" class="btn btn-secondary" data-dismiss="modal">閉じる</button>
                            <button type="button" class="btn btn-primary"
                                onclick="location.href='{{ route('delete', ['id' => $memo->id])}}'">
                                削除
                            </button>
                        </div>
                    </div>
                </div>
            </div>
            @endforeach
        </tbody>
    </table>
</div>
@endsection

submitページのボタンを新規作成の時と編集の時で変える

新規作成の時は$memo->id == 0で、編集の時は$memo->id != 0なので、if文で条件分岐する。
resources/views/submit.blade.phpの追加ボタンを以下のように書き換える。

@if($memo->id == 0)
<button type="submit" class="btn btn-success">追加</button>
@else
<button type="submit" class="btn btn-success">適用</button>
@endif

テーブルの列をクリックしたら、詳細画面に飛べるようにする

下のコードを、app.blade.phpのbodyタグの一番下に追加する。

resources/views/layouts/app.blade.php
<script>
    jQuery(function($) {
                $('tbody tr[data-href]').addClass('clickable').click(function() {
                    window.location = $(this).attr('data-href');
                }).find('a').hover(function() {
                    $(this).parents('tr').unbind('click');
                }, function() {
                    $(this).parents('tr').click( function() {
                        window.location = $(this).attr('data-href');
                    });
                });
            });
</script>

home.blade.phpの編集ボタンを消して、trタグにdata-href属性を追加する。

resources/views/home.blade.php
<tr data-href="{{route('submit', ['id' => $memo->id])}}">
       <td class="left">{{$memo->title}}</td>
       <td><a type="button" data-toggle="modal" data-target="#modal{{$memo->id}}">削除</a></td>
</tr>

テーブルの列をホバーすると色が変わるようにする

trタグにclass="table-row"を追加する。

resources/views/home.blade.php
<tr data-href="{{route('submit', ['id' => $memo->id])}}" class="table-row">
       <td class="left">{{$memo->title}}</td>
       <td><a type="button" data-toggle="modal" data-target="#modal{{$memo->id}}">削除</a></td>
</tr>
resources/views/home.blade.php
.table-row {
    background-color: #fff;
}

.table-row:hover {
    background-color: #ddd;
}

追加ボタンをいい感じにする。ヘッダーも固定する。

app.blade.phpでFont Awesomeを読み込む。

resources/views/layouts/app.blade.php
<link href="https://use.fontawesome.com/releases/v5.6.1/css/all.css" rel="stylesheet">

app.blade.phpを以下のように編集する。

resources/views/layouts/app.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>MemoApp</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.0/css/bootstrap.min.css"
        integrity="sha384-SI27wrMjH3ZZ89r4o+fGIJtnzkAnFs3E4qz9DIYioCQ5l9Rd/7UAa8DHcaL8jkWt" crossorigin="anonymous">
    <link href="https://use.fontawesome.com/releases/v5.6.1/css/all.css" rel="stylesheet">
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
        integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous">
    </script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
        integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous">
    </script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.0/js/bootstrap.min.js"
        integrity="sha384-3qaqj0lc6sV/qpzrc1N5DC6i1VRn/HyX4qdPaiEFbn54VjQBEU341pvjz7Dv3n6P" crossorigin="anonymous">
    </script>
</head>

<style>
    header {
        background-color: #000;
        background-position: 0px 50px;
        padding-left: 20px;
        font-size: large;
        width: 100%;
        height: 50px;
        clear: both;
        position: fixed;
        top: 0;
        margin-top: 0px;
        z-index: 1;
    }

    .title {
        position: absolute;
        top: 10px;
        color: #fff;
    }

    .container {
        margin-top: 70px;
    }
</style>

@yield('css')

<body>
    <header>
        <span class="title">MemoApp</span>
    </header>

    <div class="container">
        @yield('content')
    </div>

    <script>
        jQuery(function($) {
                $('tbody tr[data-href]').addClass('clickable').click(function() {
                    window.location = $(this).attr('data-href');
                }).find('a').hover(function() {
                    $(this).parents('tr').unbind('click');
                }, function() {
                    $(this).parents('tr').click( function() {
                        window.location = $(this).attr('data-href');
                    });
                });
            });
    </script>
</body>

</html>

home.blade.phpを以下のように編集する。

resources/views/home.blade.php
@extends('layouts.app')

@section('css')
<style>
    .table-row {
        background-color: #fff;
    }

    .table-row:hover {
        background-color: #ddd;
    }

    .card {
        margin-top: 40px;
    }

    .circle-button {
        position: fixed;
        display: inline-block;
        text-decoration: none;
        color: rgba(152, 152, 152, 0.43);
        width: 80px;
        height: 80px;
        line-height: 80px;
        font-size: 40px;
        border-radius: 50%;
        text-align: center;
        overflow: hidden;
        font-weight: bold;
        background-image: linear-gradient(#e8e8e8 0%, #d6d6d6 100%);
        text-shadow: 1px 1px 1px rgba(255, 255, 255, 0.66);
        box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.5), 0 2px 2px rgba(0, 0, 0, 0.19);
        border-bottom: solid 2px #b5b5b5;
    }

    .circle-button i {
        line-height: 80px;
    }

    .circle-button:active {
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5), 0 2px 2px rgba(0, 0, 0, 0.19);
        border-bottom: none;
    }

    @media screen and (min-width:0px) {
        .circle-button {
            bottom: 20px;
            right: 30px;
        }
    }

    @media screen and (min-width:768px) and (max-width:1024px) {
        .circle-button {
            bottom: 20px;
            right: 50px;
        }
    }

    @media screen and (min-width:1024px) {
        .circle-button {
            bottom: 30px;
            right: 150px;
        }
    }

    .delete-button {
        display: inline-block;
        text-decoration: none;
        width: 20px;
        height: 20px;
        line-height: 20px;
        font-size: 7px;
        border-radius: 50%;
        text-align: center;
        overflow: hidden;
        background-color: #cd5c5c;
        margin-right: 10px;
        margin-top: 5px;
    }

    .delete-button i {
        line-height: 20px;
        color: #fff;
    }

    .td-left {
        width: 10px;
    }
</style>
@endsection

@section('content')
<div class="card" style="width: 100%;">
    <div class="card-header">
        メモ一覧
    </div>

    <table class="table">
        <tbody>
            @foreach ($memos as $memo)
            <tr data-href="{{route('submit', ['id' => $memo->id])}}" class="table-row">
                <td class="td-left">
                    <a class="delete-button" type="button" data-toggle="modal" data-target="#modal{{$memo->id}}">
                        <i class="fa fa-minus"></i>
                    </a>
                </td>
                <td class="td-right">
                    {{$memo->title}}
                </td>
            </tr>
            <!-- Modal -->
            <div class="modal fade" id="modal{{$memo->id}}" tabindex="-1" role="dialog" aria-hidden="true">
                <div class="modal-dialog" role="document">
                    <div class="modal-content">
                        <div class="modal-header">
                            <h5 class="modal-title">以下のメモを本当に削除しますか?</h5>
                            <button type="button" class="close" data-dismiss="modal">
                                <span aria-hidden="true">&times;</span>
                            </button>
                        </div>
                        <div class="modal-body">
                            {{$memo->title}}
                        </div>
                        <div class="modal-footer">
                            <button type="button" class="btn btn-secondary" data-dismiss="modal">閉じる</button>
                            <button type="button" class="btn btn-primary"
                                onclick="location.href='{{ route('delete', ['id' => $memo->id])}}'">
                                削除
                            </button>
                        </div>
                    </div>
                </div>
            </div>
            @endforeach
        </tbody>
    </table>
</div>

<a href="{{route('submit')}}" class="circle-button">
    <i class="fa fa-plus"></i>
</a>

@endsection

submit.blade.phpのcss部分を消しておく。

resources/views/submit.blade.php
@extends('layouts.app')

@section('content')
<form method="POST" action="{{ route('submit', ['id' => $memo->id])}}">
    @csrf
    <div class="form-group">
        <label for="title">タイトル</label>
        <input type="text" class="form-control" id="title" name="title" value="{{$memo->title}}">
    </div>
    <div class="form-group">
        <label for="content">内容</label>
        <input type="text" class="form-control" id="content" name="content" value="{{$memo->content}}">
    </div>
    <a href="{{ route('home')}}" class="btn btn-primary">戻る</a>
    @if($memo->id == 0)
    <button type="submit" class="btn btn-success">追加</button>
    @else
    <button type="submit" class="btn btn-success">適用</button>
    @endif
</form>
@endsection

スクリーンショット 2020-01-04 23.29.44.png

今回はここまで。次回は、ログイン機能を実装します。

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

PHPでは未定義の定数は文字列として処理される

要約

  • PHP では 未定義の定数文字列 として処理続行する
  • Warning を無視するべからず
  • ついに PHP 8 からは動かなくなる
  • みんな PHPStan を使おう!

問題

突然ですが、PHP でこう書いたとき、どのように動くでしょうか?

echo date(Ymd);

これを書いた人は、おそらく date("Ymd") と書きたかったのでしょう。

うん、 気持ちは 解る。

一見すると、ダブルクォーテーションが前後にないので、エラー終了しそうです。

ところが、実際には 警告は出力されるが処理続行します。

PHP Warning:  Use of undefined constant Ymd - assumed 'Ymd' (this will throw an Error in a future version of PHP) in test.php on line 3
20191231

理由

未定義の定数?

処理結果を見てみると、 Use of undefined constant Ymd (未定義の 定数 Ymd の使用)と書いてあります。

どうやら、PHP は Ymd定数 として認識しているようです。

定数というと「全て大文字のアルファベット」という慣習に親しみすぎて、それ以外は定数でないかのように思ってしまいますが、定数名の定義は以下のとおりです( PHP マニュアル )。

有効な定数の名前は、 文字またはアンダースコアで始まり、任意の数の文字、数字、 アンダースコアが後に続きます。

なるほど。たしかに Ymd は定数ですね。

なんで処理続行したの?

しかし、先程のコードは、定数 Ymd が未定義であるにもかかわらず、処理続行しています。

試しに、こんな風に直接 var_dump() してみましょうか。

var_dump(Ymd);
// --> string(3) "Ymd"

未定義の定数なのに 文字列 として処理されている!

こんな仕様あるの?

はい。ちゃんと マニュアル にも書いてありました。

未定義の定数を使用した場合、ちょうどstringとして コールしたかのように(CONSTANT vs "CONSTANT")、 PHPはその定数自体の名前を使用したと仮定します。 このような振る舞いは、PHP 7.2.0 以降では非推奨となり、E_WARNING が発生するようになりました。 (それより前のバージョンでは、 E_NOTICE が発生していました)

ここで重要なのは、E_NOTICE だろうが E_WARNING だろうが 処理は続行する ということです。

問題2

先程は、たまたま期待どおり動いていました。試しに、こう書いてみたら、どうでしょうか? ログファイルに現在時刻を書き込んでいくだけの簡単なプログラムです。

define("ERROR_LOG_FILE", "error.log");

// 定数名間違えちゃった!
$fp = fopen(EROR_LOG_FILE, "a");
fwrite($fp, date("Ymd H:i:s") . PHP_EOL);
fclose($fp);

これを実行すると、ディレクトリの中に EROR_LOG_FILE というファイルが作られます。

だいたい、こういうのは単体テストで見つかるのですが、1回、そのままリリースしてしまったことがあってですね・・・1

テスト担当者は なんかたくさん Warning が出てるなー ぐらいにしか思わなかったらしく・・・

たかが Warning、されど Warning 。Warning を無視するべからず。

PHPStan

最近の IDE は優秀なので、未定義の定数を書くと、未定義だよと教えてくれます。
VSCode だと、定数の下に赤い波線を出してくれたり。
でも、それはまだまだ人力に頼ってしまっていると思います。もっと機械的にできないものでしょうか。

そこで、 静的解析の出番です!!!(ドンドン)

例えば、冒頭のコードを PHPStan で静的解析させてみましょう。

echo date(Ymd);

静的解析すると、以下のようにエラーが表示されます(実際の解析結果は こちら )。

+--------------------------------+
| Line | test.php                |
+--------------------------------+
| 3    | Constant Ymd not found. |
+--------------------------------+

こうして地上に平和がもたらされたわけです。IN TERRA PAX!

今後

PHP 7.2 から、Warning に this will throw an Error in a future version of PHP と表示されるようになりました。
そして、ついに、PHP 8 では、もはや処理続行を許さず、 Error として例外が送出されるようになります。

ちなみに、この件を扱っている RFC (PHP RFC: Deprecate and Remove Bareword (Unquoted) Strings)を見ると、以下のようなコードが載っています。

$foo = flase; // typo!
// ...
if ( $foo ) {
   var_dump($foo); // string(5) "flase"
}

$foofalse ならば、 if 文の中のコードは実行されないはずです。なのに flase とタイプミスしたせいで真逆の結果になるという・・・。おそろしい・・・。

RFC によると、この奇妙な動作は、1998 年にリリースされた PHP 3.0 で導入されたそうです。どうして PHP 3.0 で導入されたのか?

The old source code also includes the documentation with which PHP 3 shipped, which seem to have no mention of this behaviour, and no examples which take advantage of it.

古いソースコードには、そのときのドキュメントも含まれている。だけど、この動作については何も言及されていないし、それを使った例も記載されていないようだ。

何も言えねえ・・・

こうして、約 20 年間、数多の開発者が陥ってきた落とし穴は、全会一致(41 vs 0)で廃止されることになりました。南無〜

余談

PHPStan で以下のコードを静的解析してみました(実際の解析結果は こちら )。

define("ERROR_LOG_FILE", "error.log");

// 定数名間違えちゃった!
$fp = fopen(EROR_LOG_FILE, "a");
fwrite($fp, date("Ymd H:i:s") . PHP_EOL);
fclose($fp);

すると、このように出力されたのですが、6行目と7行目がどうしてエラーになるのかが分からないです。
$fp は明らかに resource 型だと思うんですけどねぇ・・・

+------------------------------------------------------------------------------------+
| Line | test.php                                                                    |
+------------------------------------------------------------------------------------+
| 5    | Constant EROR_LOG_FILE not found.                                           |
| 6    | Parameter #1 $fp of function fwrite expects resource, resource|false given. |
| 7    | Parameter #1 $fp of function fclose expects resource, resource|false given. |
+------------------------------------------------------------------------------------+

  1. 「テストコード書いてないの?」とツッコミがありそうなので、先に書いておきます。令和のこの時代にPHP5.3を使っているような現場で、テストコードを書くという基本的人権が保証されているわけないじゃないですか・・・ 

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

JavaScriptでfor文(foreach)内でのgetElementsByClassNameの使用法

はじめに

趣味で作っているツイッタークローンにおいて、ツイート一覧にマスオーバーさせると背景色が変化するjsの処理を書きたかった。

JSをあてるPHPファイル

3行目のtweetcardが対象のclass名です。
注)実際は生のphpファイルではなくテンプレートエンジンのbladeを使用しています。

home.php
@if ($posts)
  @foreach($posts as $post)
    <div class="card tweetcard">
      <div class="card-body alert tweet">
        {{ $post->name }}
        {{ $post->created_at }}
        <br>
        {{ $post->content }}
      </div>
    </div>

間違った書きかた

samle.js
const tweetcard = document.getElementsByClassName('tweetcard');

  //クリックイベントで背景色を変える
  tweetcard.addEventListener('mouseenter', () => {
      tweetcard.style.backgroundColor  = "#e6ecf0";
  }, false);

}
  //クリックイベントで背景色を戻す
  tweetcard.addEventListener('mouseleave', () => {
      tweetcard.style.backgroundColor = "white";
  }, false);
}

エラーが出ました。
リファレンスを確認してみます。下記抜粋。

getElementsByClassName メソッドは、指定されたクラス名をすべて持つすべての子要素の配列風オブジェクトを返します。 引用元:MDN web docs

getElementsByClassNameで取得した要素は配列だったのですね!

だったらfor分で回してあげればいいですね。

正しい書き方

samle.js
const tweetcard = document.getElementsByClassName('tweetcard');

//mouseenter
for(let i = 0; i < tweetcard.length; i++){

  //クリックイベントで背景色を変える
  tweetcard[i].addEventListener('mouseenter', () => {
      tweetcard[i].style.backgroundColor  = "#e6ecf0";
  }, false);

}
//mouseenter
for(let i = 0; i < tweetcard.length; i++){

  //クリックイベントで背景色を戻す
  tweetcard[i].addEventListener('mouseleave', () => {
      tweetcard[i].style.backgroundColor = "white";
  }, false);
}

これでマウスをかざした領域の背景色が変更され、マウスを外せば背景色が元に戻るようになりました。

参考

MDN web docs

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

composerのライブラリ達にいつもあてたいパッチがあるならワンシェルにしておこう

LaravelでRFCに軽く違反してるメアドにメール送ろうとしたら、Swift_RfcComplianceExceptionが発生して送れなかったんですよ。
原因は下記だったんですが、vendor配下って普通git管理しないじゃないですか。
https://stackoverflow.com/questions/1490042/swift-mailer-error-swift-rfccomplianceexception-on-an-email-that-actually-work
だから、リリースする時はcomposer installしたらパッチをあててやんなきゃいけなくて面倒ですよね。
そういう時は、patchファイルを用意して、patchコマンドを実行ってのもいいですが、なんかサクッと実行したいので、1つのshにするのがおすすめ!

$PROJECT_ROOT/vendor.patch.sh
#!/bin/bash
patch app/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Headers/MailboxHeader.php <<'PATCH'
355,357c355,357
<             throw new Swift_RfcComplianceException(
<                 'Address in mailbox given ['.$address.'] does not comply with RFC 2822, 3.6.2.'
<             );
---
> //            throw new Swift_RfcComplianceException(
> //                'Address in mailbox given ['.$address.'] does not comply with RFC 2822, 3.6.2.'
> //            );
PATCH

# 他にpatchが増えたら追記しよう

邪魔にならなく控え目でいいでしょ?

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