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

Laravel+Guzzleを使用してAPIトークンを取得する方法

こんにちは、くりぱんです。

この記事で実現できること

  • Guzzleのインストール
  • Laravel+GuzzleでAPIトークンの取得

説明

今回はPHPのHTTPクライアントであるGuzzleを使用して、とあるAPIのトークンを取得していきたいと思います。

開発環境

  • Laravel: 6.2
  • Guzzule: 7.2

実装の流れ

  1. Laravelプロジェクトの作成
  2. Guzzleのインストール
  3. 日本時間にする
  4. データベース設定
  5. Route
  6. Controller
  7. View

実装

1. Laravelのプロジェクトの作成

$ composer create-project --prefer-dist laravel/laravel testAPI "6.*"

testAPIというLaravel6のプロジェクトが作成されます。

2. Guzzleのインストール

$ composer require guzzlehttp/guzzle

composer.jsonで確認していきます。

composer.json
{

    ーー省略ーー

    "license": "MIT",
    "require": {
        "php": "^7.2.5|^8.0",
        "fideloper/proxy": "^4.4",
        "guzzlehttp/guzzle": "^7.2", // ここチェック
        "laravel/framework": "^6.20",
        "laravel/tinker": "^2.5"
    },

    ーー省略ーー

}

私の場合composer.jsonには"guzzlehttp/guzzle": "^7.2"となっていますが、バージョンが多少異なっていても特に問題はありません。

3. 日本時間にする

APIトークンの有効期限も画面に出したいので、一応日本時間にしておきます。
config/app.phpを編集します。

app.php
return [

    ーー省略ーー

    'timezone' => 'Asia/Tokyo',

    ーー省略ーー

]

4. データベース設定

今回はMySQLを使用していきます。
mysql -u root -pなどでMySQLに入り、test_apiというデータベースを作成してください。

mysql> CREATE DATABASE test_api;

.envに作成したデータベースの設定をしていきます。

.env
// データベース関連のところを抜粋
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=test_api // 今回作成したデータベース名
DB_USERNAME=ユーザー名
DB_PASSWORD=パスワード

5. Route

routes/web.phpでルートの設定を行っていきます。

web.php
<?php

Route::get('/', 'TestApiController@index'); // ここを追記

http://127.0.0.1:8000/にアクセスするとこれから作成するTestApiControllerindexメソッドが走るようになりました。

6. Controller

$ php artisan make:controller TestApiController

このコマンドで、以下のようなapp/Http/Controllers/testApiController.phpが作成されます。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TestApiController extends Controller
{
    //
}

今回は例として下記の情報で接続したと過程します。

  • ベースURL: openapi.test.api.jp
  • HTTP リクエスト: POST openapi.test.api.jp/v2/test
  • API_KEY: hjfdksaJhjfkdw134fjdklsaKJ93JKL
  • user_id: user_id
  • password: password

先程コマンドで作成したapp/Http/Controllers/testApiController.phpを編集していきます。

TestApiController.php
<?php

namespace App\Http\Controllers;

use Facade\Ignition\QueryRecorder\Query;
use Illuminate\Http\Request;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Header;

class TestApiController extends Controller
{
    public function index()
    {
        // ベースURL
        $base_url = 'openapi.test.api.jp';

        // インスタンス作成
        $client = new Client([
            'base_url' => $base_url,
        ]);

        // API_KEY
        $api_key = 'hjfdksaJhjfkdw134fjdklsaKJ93JKL';

        // オプション
        $options = [
            // デバック(デバックしたい時は記述)
            'debug' => true,

            // 
            // パラメーター(Header)
            'headers' => [
                'api-key' => $api_key,
            ],

            // パラメーター(Query)
            'query' => [
                'user_id' => 'user_id',
            ],

            // パラメーター(Body)
            'json' => [
                'password' => 'password',
            ],
        ];

        // パス
        $path = '/v2/test';

        // リクエストするURL(openapi.test.api.jp/v2/test)
        $send_url = $base_url . $path;

        $response = $client->request('POST', $send_url, $options);

        // JSONデータとして取得
        $json = $response->getBody();

        // JSONデータを連想配列にする
        $api_token = json_decode($json, true);

        return view('welcome', compact('api_token'));
    }
}

passwordはTypeがBodyだったのですが、json指定だとうまい感じで変換してくれます。

7. View

JSONを連想配列で取得しているので、有効期限とAPIトークンをそれぞれ取得していきます。
resources/views/welcome.blade.phpを下記の通り編集してください。

welcome.blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>Laravel</title>

    </head>
    <body>
        <p>有効期限:{{ date('Y年m月d日 H時i分s秒', $api_token['expires_at']) }}</p>
        <p>APIトークン:{{ $api_token['token'] }}</p>
    </body>
</html>

有効期限はミリ秒で取得されるので、date関数でフォーマットを整えています。
これで、php artisan serveしてからブラウザで確かめると、無事有効期限とAPIトークンを取得できるかと思います。

最後に

APIはまだまだわからないことばかりなので、ぜひご指摘等ありましたらコメント欄にお願いいたします。

少しでも役に立った!という時は、LGTMをポチッと、、、笑
1つでもLGTMが付くとその日がハッピーになるんです!
役に立たなかった時は、怒らないでコメント頂けると幸いです笑

Twitterもやってます!
プログラミングや金融知識についてやエンジニアの現実についてつぶやいています!
よかったら見てみてくださいね!

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

PHP/MySQLで作ったポートフォリオの振り返り

初めに

開発環境はmacOSで
PHPのバージョンは7.3を使用しています。
あくまで、アウトプットを目的として書いた記事なので、理解しにくい点、間違った内容があるかもしれません。ご容赦ください。

ポートフォリオについて

名前は『BookList』というアプリで、本のECサイトという設定で開発しました。
ECサイトは、CRUD機能だけでなく、より便利に理想的なものを作ろうとすると、
たくさんの機能が追加でき、すでに世の中にお手本となるフリマサイト、ECサイトがたくさんあるので自分もECサイトの開発に挑戦してみたいと思いました。

アプリの機能一覧

No 機能 機能について
1 新規登録機能
2 ログイン機能
3 ゲストログイン機能 新規登録しなくても、予め用意されているユーザー
4 パスワードのハッシュ化 この機能は記事にまとめました
5 商品の出品機能 商品の状態、種類、画像、タイトル、値段、著者などを登録
6 商品の編集 出品した、個数、値段、PR文、ステータスなどを編集
7 商品の削除 出品者は自分の商品を削除
8 コメント機能 商品の購入前に、出品者に対してコメントできる
9 コメント削除機能
10 商品の検索機能 商品の状態、種類で商品を検索できる
11 ページネーション 商品一覧ページ、商品別コメントページにそれぞれ
12 カートに追加 商品をカートに追加
13 カート削除 カートに追加した商品を削除
14 商品購入機能 商品を購入する

ECサイトで必須となる、出品->カートに追加->購入機能を中心に、本を扱うアプリなので、本の状態や、著者、コメント機能なども、追加していきました。

DB設計

booklist ER図.png

カラムはidしか表示させていませんが、DB設計を通じて、主キーや外部キーの繋がりを理解し、論理的に開発することができました。

セキュリティ、脆弱性

このポートフォリオは、フレームワークを使わずに、生のPHPとMysqlのみで作ったので、セキュリティ、脆弱性についてはしっかりと対策しました。

クロスサイトスクリプティング(XSS)

//xss対策
function h($str){
  return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}

上記の関数を作り、画面上にecho printを使って、値を出力する時は必ずh(hogehoge)とエスケープ処理をしました。

SQLインジェクション

これに関しては、formからpostされてきた値をそのまま、保存するのではなく、
変数に格納し、PDOのprepareメソッドを利用して作成したステートメントに値をバインドさせ対策しました。

クロスサイトリクエストフォージェリ (CSRF)

主な脆弱性の一つのcsrfに対しては、post送信を行う全てのformに対して、ランダムな文字列を生成し、post送信の受け取って操作をする前に、そのトークンの照合行い、異なる文字列の場合には、ログインページに飛ばすように処理しました。
トークンの文字列が盗まれ、再利用されないように、トークンの破棄も忘れず、実装しています。

工夫した点

1.MVCを意識する

フレームワークを使っていなくてもmodelviewcontrollerとそれぞれのフォルダを作り、データの流れを意識しながら、開発を進めました。

2.高品質なコーディングを心がける

なるべくわかりやすい関数名、変数名を命名し、require,includeなどの関数を用いて、templeteファイルなどで共通化を取り入れました。さらにコメントを積極的に、コード内に書き込み可読性の高いコーディングに仕上げました。

参考 github

デプロイについて

conohaのvpsを使って、デプロイしました。
ssh接続し、本番環境にファイル、データベースの設置が完了したら、
セキュリティを高めるために、rootログインを禁止にし公開鍵認証の設置、filewallの設定でポート番号を変更しました。
そして、ドメインを購入して、conohaのIPアドレスに割り当てました。

BookList

課題

デザイン面

バックエンドエンジニアを目指しているので、最低限のデザインしかしていない

DBの発行のしすぎ

重複したDBの値などもあり、処理が増えてしまっている

まとめ

・フレームワークを使っていないので、多機能なアプリとは言えないが、あえてPHPのみでの開発でプログラミングの楽しさや、エラーが発生した時の自走力が身につきました。

・セキュリティに関する知識、対策、SQLの理解なども深めることができました。フレームワークを利用すると、あまり直接SQL文を書くことがないのでとても勉強になったし、汎用性のきく知識だと感じています。

最後まで、読んでいただきありがとうございました!!

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

【未経験者】PHPとLaravelそれぞれで類似ポートフォリオ作ってみた

はじめに

こんにちは、おーもとと申します。エンジニアに転職をするため学習している初学者です。
私は車が好きで、「近年の若者の車離れ」という問題にフォーカスしたアプリを制作しようと思いました。
色々あって、生のPHPとLaravelの二通りの類似ポートフォリオを制作したので、記事にしてみました。

制作背景

若者が車を持たない理由には様々な理由があると思いますが、
「欲しいと思えるほど魅力を感じる車に出会っていないからなのでは?」
と思い、
・かわいいやかっこいいというスタイル
・大きさ
・国産か外車か
・アウトドアや街乗りという用途
これらの項目に当てはまる車を、結果として表示するアプリを制作することにしました。
(これらの特徴は全て私が定めているため、投票などにより特徴を決める機能をつけたいです)

11月 PHPでアプリ開発

10月からPHPの学習を始めていたので、そのアプリはPHPで制作しました。
カーセンサーAPIを使用して、車の情報を取得します。
解説動画:https://www.youtube.com/watch?v=ZXbgUtjxKM8
スクリーンショット 2020-12-12 12.34.26.png

機能

ユーザー登録関連
⚪︎ ログイン
⚪︎ ログアウト
⚪︎ 新規登録
⚪︎ ユーザー件数を表示

車の検索機能
⚪︎ 車の見た目→「かわいい」「かっこいい」「シンプル」「おしゃれ」「レトロ」
⚪︎ 車のサイズ→「ふつう」「すごくおおきい」「おおきい」「ちいさい」
⚪︎ 車の製造国→「国産車」「外車」
⚪︎ 車の用途 →「街乗り」「アウトドア」「スポーツ」

カーセンサーAPI連携
⚪︎ DBにある車情報と合致した車情報を取得
⚪︎ cronでキャッシュファイル自動生成

苦労した点

検索結果の画像表示高速化

検索の度にAPIからデータを取得していたので、電波の悪い場所では結果の表示に1分以上かかっていました。
そのため、毎日APIからデータを取得するバッチ処理をcronで自動化し、キャッシュ化することで、ユーザーにストレスのない速度で結果を表示させることができました。

EC2へデプロイ

公式ドキュメントを参考にしデプロイしました。
その際、インフラの知識が不足していたため、デプロイに一週間以上かかりました。

APIのサービス終了!!

転職活動を始めようとした際、一週間後にカーセンサーAPIサービスが終了すると知りました。
急いで提供元へ問い合わせたところ、
「完全に提供が終了すること」「24時間以上のキャッシュデータの保有も禁止」、ということを告げられました。
その後、他の車データAPIの提供元を調べましたが他にありませんでした。
画像だけでもどうにかならないかと思い、ト◯タや◯産などの画像利用規約を確認しましたが、
営利目的ではない&提供元のURLなどの情報を記載する
としても、利用は禁止でした。
そのためLaravelの勉強も兼ねて、画像問題を解決できるアプリの制作に取り掛かりました。

1月 Laravelでアプリ開発

12月末からLaravelの学習を始め、1月からアプリの制作に取り掛かりました。

前回のPHPで制作したポートフォリオとの違い

画像の取得にAPIを用いていましたが、ユーザーから愛車の画像を提供してもらう方針に変更し、機能の追加などを行いました。

完成

アプリのURL:https://pf-kurushira.com
(スマホサイズにも対応しています)
スクリーンショット 2021-03-14 16.03.00.png

使用技術

使用言語

⚪︎ HTML
⚪︎ CSS
⚪︎ SCSS
⚪︎ PHP 7.4.14
⚪︎ Laravel 6.20.11

インフラ

⚪︎ Github Actions 自動デプロイ
⚪︎ Docker 20.10.2 / docker-compose 1.27.4
⚪︎ nginx 1.18
⚪︎ mysql 5.7.31 / PHPMyAdmin
⚪︎ AWS ( EC2, ALB, ACM, S3, RDS, Route53, VPC, EIP, IAM)

インフラ構成図

スクリーンショット 2021-02-28 20 11 32

機能一覧

機能 概要
ユーザー管理機能 新規登録・ログイン・ログアウトができます
簡単ログイン機能 ログイン画面のゲストログインをクリックすることで、ゲストユーザーとしてログインできます
おすすめ車種検索機能 条件を選択すると、それにあった車種一覧を表示します
検索履歴機能 直近の検索履歴・結果を表示します
画像提供機能 ユーザーの所有している車の画像を提供できます
提供した画像の削除機能 提供した画像を削除できます
提供画像一覧表示機能 自身が提供した画像一覧を表示します。
ユーザー情報編集機能 ご登録いただいたユーザー名・メールアドレスを変更できます
Twitterシェア機能 車の検索結果をツイートすることができます
レスポンシブ機能 スマホサイズ(320~540px)にも対応しています

DB設計

スクリーンショット 2021-02-20 19 05 58

各テーブルについて

テーブル名 説明
users 登録ユーザー情報
cars 登録車情報
histories 直近の検索結果の情報
car_images 提供画像の情報

苦労した点

ユーザー情報編集ページのバリデーション

LaravelのAuth機能のバリデーションを使いまわそうとしましたが、ブラックボックスになっていて苦労しました。
→新しくバリデーションを作成。

S3からオブジェクト削除

画像の削除機能でDBからだけでなく、S3からもオブジェクトを削除する必要があり苦労しました。
→解決方法を記事にしました
laravel6でS3に画像アップロード&削除

今後の課題

機能

機能 概要
英訳機能 Google Cloud Translation APIを利用して翻訳
通報機能 ユーザーの投票で不適切な画像を削除

技術

⚪︎ テスト
⚪︎ Dockerを用いた本番環境の構築
⚪︎ ECSへデプロイ

参考にした学習教材など

PHP/Laravel

【Udemy】PHP+MySQL(MariaDB) Webサーバーサイドプログラミング入門
【書籍】詳細!PHP 7+MySQL 入門ノート
【書籍】PHPフレームワークLaravel入門 第2版
【書籍】PHPフレームワーク Laravel実践開発
Laravel6.0(PHP7.3)+MySQL+Laradockで簡易的なECサイトを作る

AWS

【Udemy】AWS:ゼロから実践するAmazon Web Services。手を動かしながらインフラの基礎を習得

Docker

【超入門】20分でLaravel開発環境を爆速構築するDockerハンズオン

さいごに

生のPHPでひとつPFを制作したのは、基礎力が身についたので良かったと思います。
今回ポートフォリオ完成を優先したため、ECSではなくEC2へデプロイしました。
まだ課題も多いですが、ブラッシュアップしていきたく思っています。
長くなりましたが、ここまで読んでくださりありがとうございました!!

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

Docker,PHP,MySQLで構築した環境におけるDBホスト名の調べ方

以下の記事を参考に、Docker,PHP,MySQLの開発環境を構築した。

記事の手順にしたがって開発環境は構築できたのだが、mysqliでDBに接続する際に、ホスト名が分からない事態に遭遇。
ホスト名は、DBに接続して以下のコマンドを入力する事で確認できる。

mysql> show variables like 'hostname';

今回はDocker環境なので、DBコンテナに入ってから上記コマンドを入力する必要がある。
入力するコマンドは以下の通り。

// DBコンテナに接続
docker-compose exec db bash

// DBに接続
root@2918fd6815ab:/# mysql -u root -p
→ パスワードを入力

// ホスト名を表示
mysql> show variables like 'hostname';

以上のコマンドで、ホスト名を確認できる。

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

◆◆ 【PHP8.1】PHPで簡単に非同期処理を書けるようになる

PHPは長きにわたり同期的、すなわち、あらゆる処理を上から順に実行していくというスタイルを取ってきました。

しかしたとえば、複数のURLからデータを取ってきて結果をまとめたいといった場合、時間のかかるHTTPリクエストは同時に投げたいですよね。
この用途にはGuzzleというライブラリが存在し、これを使えば同時にリクエストを投げられます。
しかし、ではHTTPアクセスとDBアクセスを同時にやりたい場合は?
時間のかかる計算を裏でやりたい場合は?
などと考え始めると、こういった個別のライブラリでは対処しきれません。

ということで汎用的な非同期処理をPHPで書けるようにするRFCが提出されました。

PHP RFC: Fibers

Introduction

人類史上ほぼ全ての期間において、人々はPHPを同期的なコードとしてのみ書いてきました。
同期的に実行されるコードのみが存在し、そしてそれを同期的に呼び出します。
同期関数は、関数が結果を返すまで処理が止まります。

最近では、非同期のPHPコードを書くことを可能にするプロジェクトが複数存在します。
非同期関数は、コールバックを受け取ったり、Promiseのように後で値が確定するプレースホルダを返すなどして、後で結果が得られたときにコードを実行します。
これにより、最終結果を待たずに処理を続行することができるようになりました。
プロジェクトの例としてはamphpReactPHPGuzzleなどが挙げられます。

このRFCが対処しようとしている課題は説明するのが難しいのですが、言うならばてめえの血はなに色だ?問題とでも言いましょうか。

リンク先の記事で説明されている問題を簡単にまとめると、
・非同期関数は、関数を呼ぶ仕組み自体を変化させてしまう。
・非同期関数は非同期関数を呼び出すことができない。同期関数を呼び出すことはできる。
・非同期関数を呼び出すためには、コールスタック全体が非同期でなければならない。

非同期処理にPromiseやawait、yieldを使い慣れている人にとっては、こう考えることができます。
『コールスタックのどこか一カ所でもPromiseを返そうとすると、そのPromiseがいつ解決されるかわからないから、コールスタック全体がPromiseを返さなければならなくなる』

このRFCでは、コールスタック全体を汚染することなく割り込み可能な非同期処理を導入することで、同期間数と非同期関数を区別なく取り扱うことを目指します。
これは次のように実現されます。

・PHPでFiberをサポートします。
・Fiberクラス、および対応するリフレクションクラスReflectionFiberを追加します。
・エラーのためのクラスFiberErrorとFiberExitを追加します。

Fiberは、PSR-7やDoctine ORMといった既存のインターフェイス上に、透過的にノンブロッキングIOを実装することを可能にします。
これはPromiseのようなプレースホルダオブジェクトを使用しないからです。

Fibers

Fiberを使用すると、割り込み可能なフルスタックの関数を作成することができます。
これはコルーチンやグリーンスレッドなどとも呼ばれています。

Fiberは実行スタック全体を停止させるため、関数を呼び出す側は、呼び出し方を変更する必要はありません。

Fiber::suspend()を使うとコールスタックを任意に中断することができます。
Fiber::suspend()の呼び出しは、関数の深い入れ子の奥にあるかもしれないし、あるいはどこにも無いかもしれません。

スタックを持たないジェネレータと異なり、Fiberはそれ自身のコールスタックを保持しているため、深い入れ子の関数呼び出し中であろうと一時停止することができます。
Generatorインスタンスを返さなければならないジェネレータと異なり、Fiberを使う関数は戻り値の型を変更する必要はありません。

Fiberはarray_mapやIteratorのforeach、PHPのVM内からのコールなど、任意の関数呼び出しで一時停止することができます。
一時停止したFiberを再開するには、Fiber->resume()で続けるか、もしくはFiber->throw()で例外をスローします。、
値はFiber::suspend()でreturnされるか、もしくは例外がスローされます。

Proposal

Fiber

FiberはPHPコアで定義され、シグネチャは以下のようになります。

final class Fiber
{
    /**
     * @param callable $callback コールバック関数
     */
    public function __construct(callable $callback) {}

    /**
     * 実行を開始する
     *
     * @param mixed ...$args コールバック関数に渡す引数
     *
     * @return mixed 一時停止した場合はsuspension point。終了したらnull
     *
     * @throw FiberError 既に実行されている場合。
     * @throw Throwable その他
     */
    public function start(mixed ...$args): mixed {}

    /**
     * 中断しているFiberを再開する。
     *
     * @param mixed $value
     *
     * @return mixed 一時停止した場合は次のsuspension point。終了したらnull
     *
     * @throw FiberError まだ実行されていない、現在一時停止されていない、既に終了している。
     * @throw Throwable その他
     */
    public function resume(mixed $value = null): mixed {}

    /**
     * 例外を投げる
     *
     * @param Throwable $exception
     *
     * @return mixed 一時停止した場合は次のsuspension point。終了したらnull
     *
     * @throw FiberError まだ実行されていない、現在一時停止されていない、既に終了している。
     * @throw Throwable その他
     */
    public function throw(Throwable $exception): mixed {}

    /**
     * @return bool 既に実行開始されていればtrue
     */
    public function isStarted(): bool {}

    /**
     * @return bool 一時停止中であればtrue
     */
    public function isSuspended(): bool {}

    /**
     * @return bool 実行中であればtrue
     */
    public function isRunning(): bool {}

    /**
     * @return bool 実行が既に終わっていればtrue
     */
    public function isTerminated(): bool {}

    /**
     * @return mixed コールバック関数の返り値を返す。
     *
     * @throws FiberError まだ終了していない
     */
    public function getReturn(): mixed {}

    /**
     * @return self|null 現在実行中のFiberインスタンスを返す。
     */
    public static function this(): ?self {}

    /**
     * 実行を一時停止する。メインスレッドからは呼べない。
     *
     * @param mixed resume()かthrow()で返した値。
     *
     * @return mixed resume()に渡す値。
     *
     * @throws FiberError メインスレッドから呼んだなぢ。
     * @throws Throwable その他
     */
    public static function suspend(mixed $value = null): mixed {}
}

任意のコールバック関数をnew Fiber(callable $callback)と渡すことでFiberオブジェクトを作成します。
コールバック関数はFiber::suspend()を必ずしも呼び出す必要はありません。
入れ子のスタックの奥の方にあるかもしれませんし、あるいは一度も呼ばれないかもしれません。

作成したFiberオブジェクトは、Fiber->start(mixed ...$args)と引数を渡して起動します。

Fiber::suspend()は、現在のFiberの実行を一時停止し、処理をFiber->start()Fiber->resume()Fiber->throw()いずれかの呼び出し元に戻します。
Generatorにおけるyieldに似たようなものと考えることができます。

一時停止されたFiberは、以下2つの方法で再開できます。
- Fiber->resume()に値を渡して再開。
- Fiber->throw()に値を渡して例外。

処理が完了したFiberからは、Fiber->getReturn()でコールバック関数の返り値を取得することができます。
実行が完了していなかったり、例外が起きたときにはFiberErrorがスローされます。

Fiber::this()は現在実行中のFiberインスタンスを返します。
これによってFiberの参照を、イベントループのコールバックや一時停止中のFiberインスタンスの配列など、別の場所に持っていくことができます。

ReflectionFiber

ReflectionFiberは実行中のFiberを検査するリフレクションです。
実行前でも終了済でも、任意のFiberオブジェクトからReflectionFiberを作成可能です。
ReflectionFiberは、ReflectionGeneratorとよく似ています。

final class ReflectionFiber
{
    /**
     * @param Fiber Fiberオブジェクト
     */
    public function __construct(Fiber $fiber) {}

    /**
     * @return Fiber Fiberオブジェクト
     */
    public function getFiber(): Fiber {}

    /**
     * @return string 実行中のFiberのファイル名
     */
    public function getExecutingFile(): string {}

    /**
     * @return int 実行中のFiberの行数
     */
    public function getExecutingLine(): int {}

    /**
     * @param int $options debug_backtrace()の引数と同じ
     *
     * @return array Fiberのバックトレース。debug_backtrace()やReflectionGenerator::getTrace()と似てる。
     */
    public function getTrace(int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT): array {}

    /**
     * @return bool 開始済であればtrue
     */
    public function isStarted(): bool {}

    /**
     * @return bool 一時停止中であればtrue
     */
    public function isSuspended(): bool {}

    /**
     * @return bool 実行中であればtrue
     */
    public function isRunning(): bool {}

    /**
     * @return bool 終了していればであればtrue
     */
    public function isTerminated(): bool {}
}

Unfinished Fibers

Fiberは、通常のオブジェクト同様、オブジェクトへの参照がなくなると破棄されます。

その際、実行が完了していないFiberは、実行完了していないGenerator同様破棄されます。
破棄されたFiberはFiber::suspend()で再度呼び出すことはできません。

Fiber Stacks

各Fiberには、ヒープ上に個別のC stackとVM stackが割り当てられます。
C stackは、利用可能な場合はmmapを使用して割り当てます。
即ち、ほとんどのプラットフォームでは物理メモリはオンデマンドで消費されるということです。
各Fiberスタックは、デフォルトでは最大8Mまで割り当てられ、ini設定fiber.stack_sizeで変更可能です。
このメモリはC stackに割り当てられるものであり、PHPの使うメモリとは無関係であることに注意してください。

VM stackはGeneratorと同じ方法でメモリとCPUが割り当てられます。
VM stackは動的に変更できるので、初期状態では4Kだけ使用します。

Backward Incompatible Changes

グローバル名前空間にFiberFiberErrorFiberExitReflectionFiberクラスが追加されます。
他に後方互換性のない変更はありません。

Future Scope

現在の実装は、PHP拡張モジュール用の内部Fiber APIを提供していません。
このRFCは、ユーザスペースのFiber APIに注力しています。
内部Fiber APIについては、他の拡張モジュール開発者と協力して追加していく予定です。
SwooleなどPHP拡張モジュール開発者からのフィードバックを受け、拡張モジュールがFiberを制御できるようにする予定です。

拡張モジュールは独自にFiberのような実装を持つこともできますが、内部APIが提供されれば、PHPコアのFiber実装を使うことができるようになります。

Proposed PHP Version(s)

PHP8.1。

Examples

単純な例

以下は、fiberという文字列で一時停止するFiberの単純な例です。
この文字列は$fiber->start()から返されます。
そしてresumeに渡した値がFiber::suspend()に送られます。

$fiber = new Fiber(function (): void {
    $value = Fiber::suspend('fiber');
    echo "レジュームした。$value: ", $value, "\n";
});

$value = $fiber->start();

echo "一時停止した。$value: ", $value, "\n";

$fiber->resume('test');

// 実行結果
一時停止した$value: fiber
レジュームした$value: test

イベントループ

次は非常に単純なイベントループの例です。
データを受信するためにソケットをポーリングし、利用可能になったときにコールバックを呼び出します。
このイベントループを使うことで、データがソケット上で利用可能になったときにだけFiberを再開できるようになり、読み込みのブロッキングを回避できます。

class EventLoop
{
    private string $nextId = 'a';
    private array $deferCallbacks = [];
    private array $read = [];
    private array $streamCallbacks = [];

    public function run(): void
    {
        while (!empty($this->deferCallbacks) || !empty($this->read)) {
            $defers = $this->deferCallbacks;
            $this->deferCallbacks = [];
            foreach ($defers as $id => $defer) {
                $defer();
            }

            $this->select($this->read);
        }
    }

    private function select(array $read): void
    {
        $timeout = empty($this->deferCallbacks) ? null : 0;
        if (!stream_select($read, $write, $except, $timeout, $timeout)) {
            return;
        }

        foreach ($read as $id => $resource) {
            $callback = $this->streamCallbacks[$id];
            unset($this->read[$id], $this->streamCallbacks[$id]);
            $callback($resource);
        }
    }

    public function defer(callable $callback): void
    {
        $id = $this->nextId++;
        $this->deferCallbacks[$id] = $callback;
    }

    public function read($resource, callable $callback): void
    {
        $id = $this->nextId++;
        $this->read[$id] = $resource;
        $this->streamCallbacks[$id] = $callback;
    }
}

[$read, $write] = stream_socket_pair(
    stripos(PHP_OS, 'win') === 0 ? STREAM_PF_INET : STREAM_PF_UNIX,
    STREAM_SOCK_STREAM,
    STREAM_IPPROTO_IP
);

// ストリームをノンブロッキングモードにする
stream_set_blocking($read, false);
stream_set_blocking($write, false);

$loop = new EventLoop;

// ストリームが読み込み可能になったら、さらに別のFiberでデータ読み込みを起動
$fiber = new Fiber(function () use ($loop, $read): void {
    echo "Waiting for data...\n";

    $fiber = Fiber::this();
    $loop->read($read, fn() => $fiber->resume());
    Fiber::suspend();

    $data = fread($read, 8192);

    echo "Received data: ", $data, "\n";
});

// Fiberを実行。
$fiber->start();

// コールバックでデータ書き込み
$loop->defer(fn() => fwrite($write, "Hello, world!"));

// イベントループ実行
$loop->run();

このスクリプトの実行結果は以下のようになります。

Waiting for data...
Received data: Hello, world!

以下の図は、メインスレッドとFiber間の実行フローを表しています。
実行フローは、Fiber::suspend()Fiber->resume()を呼び出すたびに行ったり来たりします。

01.png

amphp

以下は非同期フレームワークamphp v3を利用して、非同期コードを同期コードのように記述する例です。

amphp v3はイベントループを用いて、、Fiber APIの上に非同期処理のための様々な関数やPromise、コードを実行するためのコルーチンを構築します。
amphp v3のユーザはFiber APIを直接使用する必要はありません。
必要に応じてフレームワークがFiberへの登録や一時停止などの処理を行います。
従って、他の類似のフレームワークでは作成・使用方法が多少異なる場合があります。

defer(callable $callback, mixed ...$args)関数は、現在のFiberが終了したもしくは一時停止したときに実行される次のFiberを登録します。
delay(int $milliseconds)は、現在のFiberを指定したミリ秒中断します。

use function Amp\defer;
use function Amp\delay;

// deferは新しいFiberを作り、実行中のFiberが終了したら自動的に次を実行する
defer(function (): void {
    delay(1500);
    var_dump(1);
});

defer(function (): void {
    delay(1000);
    var_dump(2);
});

defer(function (): void {
    delay(2000);
    var_dump(3);
});

// メインスレッドを一時停止
delay(500);
var_dump(4);

amphp その2

メインスレッドが一時停止している間、イベントループがどのように実行されるかを示すため、ふたたびamphp v3を用いた例を出します。
await(Promise $promise)は、引数のPromseが解決されるまで実行が停止します。
そしてasync(callable $callback, mixed ...$args)は、Promiseオブジェクトを返します。

use function Amp\async;
use function Amp\await;
use function Amp\defer;
use function Amp\delay;

// 返り値はintであり、PromiseやGeneratorと異なりコルーチンとして実行されることに注意
function asyncTask(int $id): int {
    // ここでは何もしてない。非同期IOとかのかわり。
    delay(1000); // 1秒停止するだけ
    return $id;
}

$running = true;
defer(function () use (&$running): void {
    // ほかのFiberでブロックされないことを示したいだけ
    while ($running) {
        delay(100);
        echo ".\n";
    }
});

// asyncTask()は1秒後にintを返す
$result = asyncTask(1);
var_dump($result);

// 2つのFiberを同時に実行する。await()は全部終わるまでFiberを中断する。
$result = await([  // 1秒でおわる
    async(fn() => asyncTask(2)), // async()はFiberを作成してPromiseを返す
    async(fn() => asyncTask(3)),
]);
var_dump($result);

$result = asyncTask(4); // 1秒かかる
var_dump($result);

// array_map()は2秒かかる。これは呼び出しが同時に行われないということを表す
$result = array_map(fn(int $value) => asyncTask($value), [5, 6]);
var_dump($result);

$running = false; // 上のdeferを止める

ここawaitのvar_dump($result);Executed after 2 secondsって書いてあるんだけど、これ1 secondじゃないの?
読み間違い?

Generator

FiberはPHP VMの呼び出し中にも一時停止することができるので、非同期イテレータやGeneratorを作成することもできます。
以下の例ではamphp v3を使い、Generator内でFiberを一時停止させています。
Generatorを反復処理する際、Generatorの返り値を待っている間foreachループは一時停止します。

use Amp\Delayed;
use function Amp\await;

function generator(): Generator {
    yield await(new Delayed(500, 1));
    yield await(new Delayed(1500, 2));
    yield await(new Delayed(1000, 3));
    yield await(new Delayed(2000, 4));
    yield 5;
    yield 6;
    yield 7;
    yield await(new Delayed(2000, 8));
    yield 9;
    yield await(new Delayed(1000, 10));
}

// 通常どおりGeneratorを反復しますが、必要に応じてループを一時停止します
foreach (generator() as $value) {
    printf("Generator yielded %d\n", $value);
}

// 引数アンパックも同じ
var_dump(...generator());

ReactPHP

最後はReactPHPを使って、await関数を定義する例です。

use React\EventLoop\LoopInterface;
use React\Promise\PromiseInterface;

function await(PromiseInterface $promise, LoopInterface $loop): mixed
{
    $fiber = Fiber::this();
    if ($fiber === null) {
        throw new Error('Promises can only be awaited within a fiber');
    }

    $promise->done(
        fn(mixed $value) => $loop->futureTick(fn() => $fiber->resume($value)),
        fn(Throwable $reason) => $loop->futureTick(fn() => $fiber->throw($reason))
    );

    return Fiber::suspend();
}

ReactPHPとFiberを統合するデモがtrowski/react-fiberで実装されています。

FAQ

Who is the target audience for this feature?

この機能のターゲットは誰?

Fiberは、ほとんどのユーザは直接使用することのない高度な機能です。
主にイベントループや非同期APIを提供するライブラリ・フレームワーク作者をターゲットとしています。
Fiberは、アプリケーションのコールスタックを変更したり、大きなコード変更を加えることなく、任意の時点で非同期コードを既存の同期コードにシームレスに導入することを可能にします。

Fiberは、アプリケーションレベルで直接使用することを想定していません。
Fiberはローレベルのフロー制御APIであり、アプリケーションコードで使用する高度な抽象化を提供します。
同じような事例のひとつとして、FFIは最近PHPに追加された機能の一部ですが、ほとんどのユーザは直接使用することはありません。
しかし、ユーザは使用しているライブラリを通して大きな恩恵を受けることができます。

What about performance?

パフォーマンスはどう?

Fiber間の切り替えは軽量で、プラットフォームにもよりますが、凡そ20箇所のポインタを変更するだけです。
PHP VM上での実行コンテキストの切り替えはGeneratorと似ていて、こちらも数個のポインタをスワップするだけです。
Fiberはひとつのスレッド内に存在するため、Fiberの切り替えはプロセスやスレッドの切り替えよりも高パフォーマンスです。

What platforms are supported?

サポートしているプラットフォームは?

x86、x86_64、ARM、PPC、MIPS、Windows(Fiber APIを提供しているためアーキテクチャを問わない)など大抵の最新CPU、そしてucontext対応の古いPosixをサポートしています。

How are execution stacks swapped?

実行スタックのスワップ方法は?

各FiberはC stackとVM stackのポインタを保持しています。
Fiberに入ると現在のC stackがスワップされます。
VM stackはメモリにバックアップされていて、Fiberが終了すると元に戻ります。
debug_backtrace()や例外のバックトレースは、現在のFiberのみトレースします。
Fiberの中に入ったFiberのトレースは行いません。

How does blocking code affect fibers

ブロッキングコードがFiberに与える影響。

ブロックするコード(file_get_contents()など)は、他のFiberがあっても引き続きプロセス全体をブロックします。
パフォーマンスと同時実行性の両方を実現するには、非同期IO、イベントループ、そしてFiberを使用するようにコードを記述する必要があります。
非同期IOのライブラリは既にいくつか存在していますが、それらはFiberを利用することで同期コードと統合することができます。

Fiberは非同期IOを透過的に使用できるので、ブロッキング実装を非ブロッキング実装に置き換えることがでます。
コールスタック全体に影響を与えることはありません。
将来的に内部イベントループが実装された際は、sleep()などの内部関数をデフォルトでノンブロッキングにすることも可能です。

How do various fibers access the same memory?

複数のFiberが同じメモリにアクセスするのは何故?

全てのFiberはひとつのスレッド内に存在します。
一度に実行できるFiberはひとつだけなので、メモリを同時に変更できるスレッドとは異なり、複数のFiberが同時にメモリアクセスしたり変更することはできません。

Fiberが中断・再開されると、同じメモリにアクセスする複数のFiberがインターリーブされます。
そのため、実行中のFiberが、中断されている別のFiberが私用しているメモリを変更する可能性があります。
この問題に対応する方法としては、mutexes・semaphores・memory parcels・channelsといった手段が存在します。
これらは、FiberAPIを使ってユーザベースで実装できるため、このRFCにおいては提供しません。

Why add this to PHP core?

なぜPHPコアに導入する?

この機能をPHPコアに直接追加することで、PHPを提供している全てのホストがこの機能を使うことができるようになります。
多くの場合、ユーザはホストがどのエクステンションを提供しているのかを知らなかったり、ユーザ側で任意にエクステンションを追加できなかったり、追加したくなかったりします。
FiberがPHPコアに含まれていれば、あらゆるライブラリ作者が移植性を気にすることなくFiberを利用することができます。

Why not add an event loop and async/await API to core?

イベントループとasync/awaitも追加しない?

このRFCは、フルスタックのコルーチンやグリーンスレッドをユーザコードで提供することを可能にする最低限の機能のみを提案しています。
独自のイベントループやPromise、その他非同期APIを提供しているフレームワークが幾つか存在しますが、それらのAPI設計は多様で、仕様が異なります。
それら特定のニーズのために設計されている非同期APIは、PHPコアに実装されたイベントループなどではカバーできないかもしれません。

最低限の機能をコアで提供し、ユーザ側で任意にコンポーネントを実装できるようにするのが最も良い、というのがこのRFCの主張です。
多くのフレームワークがひとつのイベントループAPIに集約されたり、PHPコアにイベントループを搭載したいという要求が出てきた場合には、次のRFCでそれらを導入することになるかもしれません。
このRFCは、今後PHPコアにasync/awaitやイベントループを追加することを妨げるものではありません。

How does this proposal differ from prior Fiber proposals?

かつてのFiber RFCとのちがいは?

かつて出されたFiberのRFCは、内部関数(array_map, preg_replace_callbackなど)やopcacheハンドラ(foreach, yieldなど)内でのコンテキスト切り替えをサポートしていませんでした。
そのため、Cコードから呼び出されるユーザコードや、Xdebugのようにzend_execute_exをオーバーライドするエクステンションを使うとクラッシュする可能性がありました。

Vote

投票期間は2021/03/08から2021/03/22です。
投票の2/3以上の賛成で受理されます。

2021/03/15時点では賛成38反対10の賛成多数であり、おそらく受理されます。

References

感想

残念ながら、ユーザが直接ばりばり並列処理を書けるようにする目的で導入されるものではなく、並列処理を書けるようにするライブラリを作れるようにするための機能です。
とはいえamphpが既にFiberに対応したバージョンを作ってしまうなど、導入の敷居も高くないようで、今後他のライブラリの対応も期待できるかもしれません。

また目的ではないとはいえ、べつに個人で導入することが禁止されているわけでもないので、何らかの並列処理する機能を作ってみるのも面白いかもしれませんね。

メーリングリストバトル

投票開始後、MLでtwoseeから長文のツッコミが入りました。
ソースコード上の無駄な呼び出しがある、他のC拡張からの割り込みを考慮していない、Windowsでboost-contextより性能の落ちるwin-fiberを使っている、状態取得メソッドがたくさんあるのにFiber::getStatus()が何故かない、Swooleと互換していない、などなど様々な指摘がなされています。
いきなりPHPコアに実用経験のない実験的機能をぶち込むのはどうなんだ、まずはSwooleのようにPECLでやるべきではないのか、SwooleはPDO、mysqli、phpredis、libcurlなど様々な機能をコルーチン化してきた実績がある、そしていつかPHP本体にマージされるのを待っている。

まあ概ね頷ける内容ではあるのですが、ただこれ実はちょっとだけ裏がありまして、この反論の主twoseeSwooleの中の人で、そしてRFCの提出者Aaron Piotrowskiamphpの中の人です。
SwooleもamphpもPHPの非同期ライブラリであり、その片側が出したRFCが通過したとなれば、Swoole側としては当然面白くないわけです。

次いでSwooleのファウンダーと名乗る人が登場
Fiberはamphpにしか使えず、他のPHPプロジェクトには価値がないぞ。PECL行きが妥当。
PHP9あたりでGo言語ばりの非同期IDとCSPを導入してくれ。

この投稿に反応して今度はさらにReactPHPの中の人登場
おいィちょっと待てよSwooleにとってFiberは役立たずかもしれんがReactPHPにはめっちゃ有用なんだが?
なにしろこれでPSR-15がフルサポートできるってもんよ。

もちろんReactPHPもPHP用非同期ライブラリです。

と楽しいライブラリ間代理戦争状態が勃発しましたが、投票経過を見るにFiber側が勝利しそうではあります。

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

【PHP8.1】PHPで簡単に非同期処理を書けるようになる

PHPは長きにわたり同期的、すなわち、あらゆる処理を上から順に実行していくというスタイルを取ってきました。

しかしたとえば、複数のURLからデータを取ってきて結果をまとめたいといった場合、時間のかかるHTTPリクエストは同時に投げたいですよね。
この用途にはGuzzleというライブラリが存在し、これを使えば同時にリクエストを投げられます。
しかし、ではHTTPアクセスとDBアクセスを同時にやりたい場合は?
時間のかかる計算を裏でやりたい場合は?
などと考え始めると、こういった個別のライブラリでは対処しきれません。

ということで汎用的な非同期処理をPHPで書けるようにするRFCが提出されました。

PHP RFC: Fibers

Introduction

人類史上ほぼ全ての期間において、人々はPHPを同期的なコードとしてのみ書いてきました。
同期的に実行されるコードのみが存在し、そしてそれを同期的に呼び出します。
同期関数は、関数が結果を返すまで処理が止まります。

最近では、非同期のPHPコードを書くことを可能にするプロジェクトが複数存在します。
非同期関数は、コールバックを受け取ったり、Promiseのように後で値が確定するプレースホルダを返すなどして、後で結果が得られたときにコードを実行します。
これにより、最終結果を待たずに処理を続行することができるようになりました。
プロジェクトの例としてはamphpReactPHPGuzzleなどが挙げられます。

このRFCが対処しようとしている課題は説明するのが難しいのですが、言うならばてめえの血はなに色だ?問題とでも言いましょうか。

リンク先の記事で説明されている問題を簡単にまとめると、
・非同期関数は、関数を呼ぶ仕組み自体を変化させてしまう。
・非同期関数は非同期関数を呼び出すことができない。同期関数を呼び出すことはできる。
・非同期関数を呼び出すためには、コールスタック全体が非同期でなければならない。

非同期処理にPromiseやawait、yieldを使い慣れている人にとっては、こう考えることができます。
『コールスタックのどこか一カ所でもPromiseを返そうとすると、そのPromiseがいつ解決されるかわからないから、コールスタック全体がPromiseを返さなければならなくなる』

このRFCでは、コールスタック全体を汚染することなく割り込み可能な非同期処理を導入することで、同期間数と非同期関数を区別なく取り扱うことを目指します。
これは次のように実現されます。

・PHPでFiberをサポートします。
・Fiberクラス、および対応するリフレクションクラスReflectionFiberを追加します。
・エラーのためのクラスFiberErrorとFiberExitを追加します。

Fiberは、PSR-7やDoctine ORMといった既存のインターフェイス上に、透過的にノンブロッキングIOを実装することを可能にします。
これはPromiseのようなプレースホルダオブジェクトを使用しないからです。

Fibers

Fiberを使用すると、割り込み可能なフルスタックの関数を作成することができます。
これはコルーチンやグリーンスレッドなどとも呼ばれています。

Fiberは実行スタック全体を停止させるため、関数を呼び出す側は、呼び出し方を変更する必要はありません。

Fiber::suspend()を使うとコールスタックを任意に中断することができます。
Fiber::suspend()の呼び出しは、関数の深い入れ子の奥にあるかもしれないし、あるいはどこにも無いかもしれません。

スタックを持たないジェネレータと異なり、Fiberはそれ自身のコールスタックを保持しているため、深い入れ子の関数呼び出し中であろうと一時停止することができます。
Generatorインスタンスを返さなければならないジェネレータと異なり、Fiberを使う関数は戻り値の型を変更する必要はありません。

Fiberはarray_mapやIteratorのforeach、PHPのVM内からのコールなど、任意の関数呼び出しで一時停止することができます。
一時停止したFiberを再開するには、Fiber->resume()で続けるか、もしくはFiber->throw()で例外をスローします。、
値はFiber::suspend()でreturnされるか、もしくは例外がスローされます。

Proposal

Fiber

FiberはPHPコアで定義され、シグネチャは以下のようになります。

final class Fiber
{
    /**
     * @param callable $callback コールバック関数
     */
    public function __construct(callable $callback) {}

    /**
     * 実行を開始する
     *
     * @param mixed ...$args コールバック関数に渡す引数
     *
     * @return mixed 一時停止した場合はsuspension point。終了したらnull
     *
     * @throw FiberError 既に実行されている場合。
     * @throw Throwable その他
     */
    public function start(mixed ...$args): mixed {}

    /**
     * 中断しているFiberを再開する。
     *
     * @param mixed $value
     *
     * @return mixed 一時停止した場合は次のsuspension point。終了したらnull
     *
     * @throw FiberError まだ実行されていない、現在一時停止されていない、既に終了している。
     * @throw Throwable その他
     */
    public function resume(mixed $value = null): mixed {}

    /**
     * 例外を投げる
     *
     * @param Throwable $exception
     *
     * @return mixed 一時停止した場合は次のsuspension point。終了したらnull
     *
     * @throw FiberError まだ実行されていない、現在一時停止されていない、既に終了している。
     * @throw Throwable その他
     */
    public function throw(Throwable $exception): mixed {}

    /**
     * @return bool 既に実行開始されていればtrue
     */
    public function isStarted(): bool {}

    /**
     * @return bool 一時停止中であればtrue
     */
    public function isSuspended(): bool {}

    /**
     * @return bool 実行中であればtrue
     */
    public function isRunning(): bool {}

    /**
     * @return bool 実行が既に終わっていればtrue
     */
    public function isTerminated(): bool {}

    /**
     * @return mixed コールバック関数の返り値を返す。
     *
     * @throws FiberError まだ終了していない
     */
    public function getReturn(): mixed {}

    /**
     * @return self|null 現在実行中のFiberインスタンスを返す。
     */
    public static function this(): ?self {}

    /**
     * 実行を一時停止する。メインスレッドからは呼べない。
     *
     * @param mixed resume()かthrow()で返した値。
     *
     * @return mixed resume()に渡す値。
     *
     * @throws FiberError メインスレッドから呼んだなぢ。
     * @throws Throwable その他
     */
    public static function suspend(mixed $value = null): mixed {}
}

任意のコールバック関数をnew Fiber(callable $callback)と渡すことでFiberオブジェクトを作成します。
コールバック関数はFiber::suspend()を必ずしも呼び出す必要はありません。
入れ子のスタックの奥の方にあるかもしれませんし、あるいは一度も呼ばれないかもしれません。

作成したFiberオブジェクトは、Fiber->start(mixed ...$args)と引数を渡して起動します。

Fiber::suspend()は、現在のFiberの実行を一時停止し、処理をFiber->start()Fiber->resume()Fiber->throw()いずれかの呼び出し元に戻します。
Generatorにおけるyieldに似たようなものと考えることができます。

一時停止されたFiberは、以下2つの方法で再開できます。
- Fiber->resume()に値を渡して再開。
- Fiber->throw()に値を渡して例外。

処理が完了したFiberからは、Fiber->getReturn()でコールバック関数の返り値を取得することができます。
実行が完了していなかったり、例外が起きたときにはFiberErrorがスローされます。

Fiber::this()は現在実行中のFiberインスタンスを返します。
これによってFiberの参照を、イベントループのコールバックや一時停止中のFiberインスタンスの配列など、別の場所に持っていくことができます。

ReflectionFiber

ReflectionFiberは実行中のFiberを検査するリフレクションです。
実行前でも終了済でも、任意のFiberオブジェクトからReflectionFiberを作成可能です。
ReflectionFiberは、ReflectionGeneratorとよく似ています。

final class ReflectionFiber
{
    /**
     * @param Fiber Fiberオブジェクト
     */
    public function __construct(Fiber $fiber) {}

    /**
     * @return Fiber Fiberオブジェクト
     */
    public function getFiber(): Fiber {}

    /**
     * @return string 実行中のFiberのファイル名
     */
    public function getExecutingFile(): string {}

    /**
     * @return int 実行中のFiberの行数
     */
    public function getExecutingLine(): int {}

    /**
     * @param int $options debug_backtrace()の引数と同じ
     *
     * @return array Fiberのバックトレース。debug_backtrace()やReflectionGenerator::getTrace()と似てる。
     */
    public function getTrace(int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT): array {}

    /**
     * @return bool 開始済であればtrue
     */
    public function isStarted(): bool {}

    /**
     * @return bool 一時停止中であればtrue
     */
    public function isSuspended(): bool {}

    /**
     * @return bool 実行中であればtrue
     */
    public function isRunning(): bool {}

    /**
     * @return bool 終了していればであればtrue
     */
    public function isTerminated(): bool {}
}

Unfinished Fibers

Fiberは、通常のオブジェクト同様、オブジェクトへの参照がなくなると破棄されます。

その際、実行が完了していないFiberは、実行完了していないGenerator同様破棄されます。
破棄されたFiberはFiber::suspend()で再度呼び出すことはできません。

Fiber Stacks

各Fiberには、ヒープ上に個別のC stackとVM stackが割り当てられます。
C stackは、利用可能な場合はmmapを使用して割り当てます。
即ち、ほとんどのプラットフォームでは物理メモリはオンデマンドで消費されるということです。
各Fiberスタックは、デフォルトでは最大8Mまで割り当てられ、ini設定fiber.stack_sizeで変更可能です。
このメモリはC stackに割り当てられるものであり、PHPの使うメモリとは無関係であることに注意してください。

VM stackはGeneratorと同じ方法でメモリとCPUが割り当てられます。
VM stackは動的に変更できるので、初期状態では4Kだけ使用します。

Backward Incompatible Changes

グローバル名前空間にFiberFiberErrorFiberExitReflectionFiberクラスが追加されます。
他に後方互換性のない変更はありません。

Future Scope

現在の実装は、PHP拡張モジュール用の内部Fiber APIを提供していません。
このRFCは、ユーザスペースのFiber APIに注力しています。
内部Fiber APIについては、他の拡張モジュール開発者と協力して追加していく予定です。
SwooleなどPHP拡張モジュール開発者からのフィードバックを受け、拡張モジュールがFiberを制御できるようにする予定です。

拡張モジュールは独自にFiberのような実装を持つこともできますが、内部APIが提供されれば、PHPコアのFiber実装を使うことができるようになります。

Proposed PHP Version(s)

PHP8.1。

Examples

単純な例

以下は、fiberという文字列で一時停止するFiberの単純な例です。
この文字列は$fiber->start()から返されます。
そしてresumeに渡した値がFiber::suspend()に送られます。

$fiber = new Fiber(function (): void {
    $value = Fiber::suspend('fiber');
    echo "レジュームした。$value: ", $value, "\n";
});

$value = $fiber->start();

echo "一時停止した。$value: ", $value, "\n";

$fiber->resume('test');

// 実行結果
一時停止した$value: fiber
レジュームした$value: test

イベントループ

次は非常に単純なイベントループの例です。
データを受信するためにソケットをポーリングし、利用可能になったときにコールバックを呼び出します。
このイベントループを使うことで、データがソケット上で利用可能になったときにだけFiberを再開できるようになり、読み込みのブロッキングを回避できます。

class EventLoop
{
    private string $nextId = 'a';
    private array $deferCallbacks = [];
    private array $read = [];
    private array $streamCallbacks = [];

    public function run(): void
    {
        while (!empty($this->deferCallbacks) || !empty($this->read)) {
            $defers = $this->deferCallbacks;
            $this->deferCallbacks = [];
            foreach ($defers as $id => $defer) {
                $defer();
            }

            $this->select($this->read);
        }
    }

    private function select(array $read): void
    {
        $timeout = empty($this->deferCallbacks) ? null : 0;
        if (!stream_select($read, $write, $except, $timeout, $timeout)) {
            return;
        }

        foreach ($read as $id => $resource) {
            $callback = $this->streamCallbacks[$id];
            unset($this->read[$id], $this->streamCallbacks[$id]);
            $callback($resource);
        }
    }

    public function defer(callable $callback): void
    {
        $id = $this->nextId++;
        $this->deferCallbacks[$id] = $callback;
    }

    public function read($resource, callable $callback): void
    {
        $id = $this->nextId++;
        $this->read[$id] = $resource;
        $this->streamCallbacks[$id] = $callback;
    }
}

[$read, $write] = stream_socket_pair(
    stripos(PHP_OS, 'win') === 0 ? STREAM_PF_INET : STREAM_PF_UNIX,
    STREAM_SOCK_STREAM,
    STREAM_IPPROTO_IP
);

// ストリームをノンブロッキングモードにする
stream_set_blocking($read, false);
stream_set_blocking($write, false);

$loop = new EventLoop;

// ストリームが読み込み可能になったら、さらに別のFiberでデータ読み込みを起動
$fiber = new Fiber(function () use ($loop, $read): void {
    echo "Waiting for data...\n";

    $fiber = Fiber::this();
    $loop->read($read, fn() => $fiber->resume());
    Fiber::suspend();

    $data = fread($read, 8192);

    echo "Received data: ", $data, "\n";
});

// Fiberを実行。
$fiber->start();

// コールバックでデータ書き込み
$loop->defer(fn() => fwrite($write, "Hello, world!"));

// イベントループ実行
$loop->run();

このスクリプトの実行結果は以下のようになります。

Waiting for data...
Received data: Hello, world!

以下の図は、メインスレッドとFiber間の実行フローを表しています。
実行フローは、Fiber::suspend()Fiber->resume()を呼び出すたびに行ったり来たりします。

01.png

amphp

以下は非同期フレームワークamphp v3を利用して、非同期コードを同期コードのように記述する例です。

amphp v3はイベントループを用いて、、Fiber APIの上に非同期処理のための様々な関数やPromise、コードを実行するためのコルーチンを構築します。
amphp v3のユーザはFiber APIを直接使用する必要はありません。
必要に応じてフレームワークがFiberへの登録や一時停止などの処理を行います。
従って、他の類似のフレームワークでは作成・使用方法が多少異なる場合があります。

defer(callable $callback, mixed ...$args)関数は、現在のFiberが終了したもしくは一時停止したときに実行される次のFiberを登録します。
delay(int $milliseconds)は、現在のFiberを指定したミリ秒中断します。

use function Amp\defer;
use function Amp\delay;

// deferは新しいFiberを作り、実行中のFiberが終了したら自動的に次を実行する
defer(function (): void {
    delay(1500);
    var_dump(1);
});

defer(function (): void {
    delay(1000);
    var_dump(2);
});

defer(function (): void {
    delay(2000);
    var_dump(3);
});

// メインスレッドを一時停止
delay(500);
var_dump(4);

amphp その2

メインスレッドが一時停止している間、イベントループがどのように実行されるかを示すため、ふたたびamphp v3を用いた例を出します。
await(Promise $promise)は、引数のPromseが解決されるまで実行が停止します。
そしてasync(callable $callback, mixed ...$args)は、Promiseオブジェクトを返します。

use function Amp\async;
use function Amp\await;
use function Amp\defer;
use function Amp\delay;

// 返り値はintであり、PromiseやGeneratorと異なりコルーチンとして実行されることに注意
function asyncTask(int $id): int {
    // ここでは何もしてない。非同期IOとかのかわり。
    delay(1000); // 1秒停止するだけ
    return $id;
}

$running = true;
defer(function () use (&$running): void {
    // ほかのFiberでブロックされないことを示したいだけ
    while ($running) {
        delay(100);
        echo ".\n";
    }
});

// asyncTask()は1秒後にintを返す
$result = asyncTask(1);
var_dump($result);

// 2つのFiberを同時に実行する。await()は全部終わるまでFiberを中断する。
$result = await([  // 1秒でおわる
    async(fn() => asyncTask(2)), // async()はFiberを作成してPromiseを返す
    async(fn() => asyncTask(3)),
]);
var_dump($result); // 2秒後に実行される

$result = asyncTask(4); // 1秒かかる
var_dump($result);

// array_map()は2秒かかる。これは呼び出しが同時に行われないということを表す
$result = array_map(fn(int $value) => asyncTask($value), [5, 6]);
var_dump($result);

$running = false; // 上のdeferを止める

ここawaitのvar_dump($result);Executed after 2 secondsって書いてあるんだけど、これ1 secondじゃないの?
読み間違い?

Generator

FiberはPHP VMの呼び出し中にも一時停止することができるので、非同期イテレータやGeneratorを作成することもできます。
以下の例ではamphp v3を使い、Generator内でFiberを一時停止させています。
Generatorを反復処理する際、Generatorの返り値を待っている間foreachループは一時停止します。

use Amp\Delayed;
use function Amp\await;

function generator(): Generator {
    yield await(new Delayed(500, 1));
    yield await(new Delayed(1500, 2));
    yield await(new Delayed(1000, 3));
    yield await(new Delayed(2000, 4));
    yield 5;
    yield 6;
    yield 7;
    yield await(new Delayed(2000, 8));
    yield 9;
    yield await(new Delayed(1000, 10));
}

// 通常どおりGeneratorを反復しますが、必要に応じてループを一時停止します
foreach (generator() as $value) {
    printf("Generator yielded %d\n", $value);
}

// 引数アンパックも同じ
var_dump(...generator());

ReactPHP

最後はReactPHPを使って、await関数を定義する例です。

use React\EventLoop\LoopInterface;
use React\Promise\PromiseInterface;

function await(PromiseInterface $promise, LoopInterface $loop): mixed
{
    $fiber = Fiber::this();
    if ($fiber === null) {
        throw new Error('Promises can only be awaited within a fiber');
    }

    $promise->done(
        fn(mixed $value) => $loop->futureTick(fn() => $fiber->resume($value)),
        fn(Throwable $reason) => $loop->futureTick(fn() => $fiber->throw($reason))
    );

    return Fiber::suspend();
}

ReactPHPとFiberを統合するデモがtrowski/react-fiberで実装されています。

FAQ

Who is the target audience for this feature?

この機能のターゲットは誰?

Fiberは、ほとんどのユーザは直接使用することのない高度な機能です。
主にイベントループや非同期APIを提供するライブラリ・フレームワーク作者をターゲットとしています。
Fiberは、アプリケーションのコールスタックを変更したり、大きなコード変更を加えることなく、任意の時点で非同期コードを既存の同期コードにシームレスに導入することを可能にします。

Fiberは、アプリケーションレベルで直接使用することを想定していません。
Fiberはローレベルのフロー制御APIであり、アプリケーションコードで使用する高度な抽象化を提供します。
同じような事例のひとつとして、FFIは最近PHPに追加された機能の一部ですが、ほとんどのユーザは直接使用することはありません。
しかし、ユーザは使用しているライブラリを通して大きな恩恵を受けることができます。

What about performance?

パフォーマンスはどう?

Fiber間の切り替えは軽量で、プラットフォームにもよりますが、凡そ20箇所のポインタを変更するだけです。
PHP VM上での実行コンテキストの切り替えはGeneratorと似ていて、こちらも数個のポインタをスワップするだけです。
Fiberはひとつのスレッド内に存在するため、Fiberの切り替えはプロセスやスレッドの切り替えよりも高パフォーマンスです。

What platforms are supported?

サポートしているプラットフォームは?

x86、x86_64、ARM、PPC、MIPS、Windows(Fiber APIを提供しているためアーキテクチャを問わない)など大抵の最新CPU、そしてucontext対応の古いPosixをサポートしています。

How are execution stacks swapped?

実行スタックのスワップ方法は?

各FiberはC stackとVM stackのポインタを保持しています。
Fiberに入ると現在のC stackがスワップされます。
VM stackはメモリにバックアップされていて、Fiberが終了すると元に戻ります。
debug_backtrace()や例外のバックトレースは、現在のFiberのみトレースします。
Fiberの中に入ったFiberのトレースは行いません。

How does blocking code affect fibers

ブロッキングコードがFiberに与える影響。

ブロックするコード(file_get_contents()など)は、他のFiberがあっても引き続きプロセス全体をブロックします。
パフォーマンスと同時実行性の両方を実現するには、非同期IO、イベントループ、そしてFiberを使用するようにコードを記述する必要があります。
非同期IOのライブラリは既にいくつか存在していますが、それらはFiberを利用することで同期コードと統合することができます。

Fiberは非同期IOを透過的に使用できるので、ブロッキング実装を非ブロッキング実装に置き換えることがでます。
コールスタック全体に影響を与えることはありません。
将来的に内部イベントループが実装された際は、sleep()などの内部関数をデフォルトでノンブロッキングにすることも可能です。

How do various fibers access the same memory?

複数のFiberが同じメモリにアクセスするのは何故?

全てのFiberはひとつのスレッド内に存在します。
一度に実行できるFiberはひとつだけなので、メモリを同時に変更できるスレッドとは異なり、複数のFiberが同時にメモリアクセスしたり変更することはできません。

Fiberが中断・再開されると、同じメモリにアクセスする複数のFiberがインターリーブされます。
そのため、実行中のFiberが、中断されている別のFiberが私用しているメモリを変更する可能性があります。
この問題に対応する方法としては、mutexes・semaphores・memory parcels・channelsといった手段が存在します。
これらは、FiberAPIを使ってユーザベースで実装できるため、このRFCにおいては提供しません。

Why add this to PHP core?

なぜPHPコアに導入する?

この機能をPHPコアに直接追加することで、PHPを提供している全てのホストがこの機能を使うことができるようになります。
多くの場合、ユーザはホストがどのエクステンションを提供しているのかを知らなかったり、ユーザ側で任意にエクステンションを追加できなかったり、追加したくなかったりします。
FiberがPHPコアに含まれていれば、あらゆるライブラリ作者が移植性を気にすることなくFiberを利用することができます。

Why not add an event loop and async/await API to core?

イベントループとasync/awaitも追加しない?

このRFCは、フルスタックのコルーチンやグリーンスレッドをユーザコードで提供することを可能にする最低限の機能のみを提案しています。
独自のイベントループやPromise、その他非同期APIを提供しているフレームワークが幾つか存在しますが、それらのAPI設計は多様で、仕様が異なります。
それら特定のニーズのために設計されている非同期APIは、PHPコアに実装されたイベントループなどではカバーできないかもしれません。

最低限の機能をコアで提供し、ユーザ側で任意にコンポーネントを実装できるようにするのが最も良い、というのがこのRFCの主張です。
多くのフレームワークがひとつのイベントループAPIに集約されたり、PHPコアにイベントループを搭載したいという要求が出てきた場合には、次のRFCでそれらを導入することになるかもしれません。
このRFCは、今後PHPコアにasync/awaitやイベントループを追加することを妨げるものではありません。

How does this proposal differ from prior Fiber proposals?

かつてのFiber RFCとのちがいは?

かつて出されたFiberのRFCは、内部関数(array_map, preg_replace_callbackなど)やopcacheハンドラ(foreach, yieldなど)内でのコンテキスト切り替えをサポートしていませんでした。
そのため、Cコードから呼び出されるユーザコードや、Xdebugのようにzend_execute_exをオーバーライドするエクステンションを使うとクラッシュする可能性がありました。

Vote

投票期間は2021/03/08から2021/03/22です。
投票の2/3以上の賛成で受理されます。

2021/03/15時点では賛成38反対10の賛成多数であり、おそらく受理されます。

References

感想

残念ながら、ユーザが直接ばりばり並列処理を書けるようにする目的で導入されるものではなく、並列処理を書けるようにするライブラリを作れるようにするための機能です。
とはいえamphpが既にFiberに対応したバージョンを作ってしまうなど、導入の敷居も高くないようで、今後他のライブラリの対応も期待できるかもしれません。

また目的ではないとはいえ、べつに個人で導入することが禁止されているわけでもないので、何らかの並列処理する機能を作ってみるのも面白いかもしれませんね。

メーリングリストバトル

投票開始後、MLでtwoseeから長文のツッコミが入りました。
ソースコード上の無駄な呼び出しがある、他のC拡張からの割り込みを考慮していない、Windowsでboost-contextより性能の落ちるwin-fiberを使っている、状態取得メソッドがたくさんあるのにFiber::getStatus()が何故かない、Swooleと互換していない、などなど様々な指摘がなされています。
いきなりPHPコアに実用経験のない実験的機能をぶち込むのはどうなんだ、まずはSwooleのようにPECLでやるべきではないのか、SwooleはPDO、mysqli、phpredis、libcurlなど様々な機能をコルーチン化してきた実績がある、そしていつかPHP本体にマージされるのを待っている。

まあ概ね頷ける内容ではあるのですが、ただこれ実はちょっとだけ裏がありまして、この反論の主twoseeSwooleの中の人で、そしてRFCの提出者Aaron Piotrowskiamphpの中の人です。
SwooleもamphpもPHPの非同期ライブラリであり、その片側が出したRFCが通過したとなれば、Swoole側としては当然面白くないわけです。

次いでSwooleのファウンダーと名乗る人が登場
Fiberはamphpにしか使えず、他のPHPプロジェクトには価値がないぞ。PECL行きが妥当。
PHP9あたりでGo言語ばりの非同期IDとCSPを導入してくれ。

この投稿に反応して今度はさらにReactPHPの中の人登場
おいィちょっと待てよSwooleにとってFiberは役立たずかもしれんがReactPHPにはめっちゃ有用なんだが?
なにしろこれでPSR-15がフルサポートできるってもんよ。

もちろんReactPHPもPHP用非同期ライブラリです。

と楽しい楽しいライブラリ間代理戦争が勃発しましたが、投票経過を見るにFiber側が勝利しそうではあります。

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

【JavaScript】axiosでCSVを受け取りブラウザでファイルダウンロードに移行させる方法。

前提

サーバーサイドはLaravel、フロントはVue.jsです。
今回はJavaScriptの話がメインなのでPHPやLaravelが分からない方でも支障なく読んでいただけると思います。

やりたかったこと

ユーザー目線:ブラウザに表示されているダウンロードボタンをクリックすると、CSVファイルをダウンロードできる。

僕目線:LaravelでCSVを吐き出すAPIを用意し、axiosを使ってAPIを叩きCSVをダウンロードできるようにする。

起こった問題

ブラウザの検索タブでエンドポイントに直接アクセスするとうまくダウンロードできるが、axiosを使ってエンドポイントにアクセスするとダウンロードできない。(CSVの内容はデータとして返ってくるが、ブラウザがダウンロード画面に移行しない。)

失敗した方法

CSVを返すAPIを単純に叩いてダウンロードできると思ったのですが、うまくいきませんでした。
※関係ない部分は色々省略してます。

sample.js
downloadCsv() {
    axios
      .get("/api/download/csv", {
      })
      .catch((error) => {
        console.log(error.messagae);
      });
  },

CSVのデータはちゃんと返ってきますが、ブラウザがダウンロードに移行しません。(ユーザー目線だと何も起こらない。)

うまくいった方法

コードを下記のようにかえるとうまくダウンロードできました。

sample.js
import saveAs from "file-saver";
// 省略
downloadCsv() {
  axios
    .get("/api/download/csv", {
      responseType: "blob",
    })
    .then((res) => {
      let mineType = res.headers["content-type"];
      const name = res.headers["content-disposition"];
      const blob = new Blob([res.data], { type: mineType });
      saveAs(blob, name);
    })
    .catch((error) => {
      console.log(error.messagae);
    });
};

追記部分を解説していきます。

リクエスト時の処理

sample.js
.get("/api/download/csv", {
  responseType: "blob",
})

データをBLOBとして受け取ります。
BLOBとはBinary Large OBjectの略で、IT用語辞典では以下のように解説されています。

BLOBとは、データベースのフィールド定義などで用いられるデータ型の一つで、テキスト(文字列)や整数のように既存のデータ型としては用意されていない任意のバイナリデータを格納するためのもの。

つまり、BlOBはテキストファイルだけではなく画像やPDFなどいろいろな形式のファイルを扱うことができる訳です。

そしてJavaScriptでは受け取ったデータをBOLBにするとこで、ファイルにすることが可能になります。

CSVはテキストデータなのでBLOBにする必要はないのでは?と思ったのですが、データをファイルにするために必要なようです。

レスポンスを受け取った後の処理

sample.js
.then((res) => {
  let mineType = res.headers["content-type"];
  const name = res.headers["content-disposition"];
  const blob = new Blob([res.data], { type: mineType });
  saveAs(blob, name);
})

res.headers["content-type"]にはLaravel側で設定した'text/csv'が格納されています。今回は必ず'text/csv'が返るようになっていますが、ダウンロード用のメソッドを色々な形式に対応できるようにするためにこのように書く必要がありますね。

res.headers["content-disposition"]にはLaravel側で設定したファイル名が格納されています。

new Blob([res.data], { type: mineType });
JavaScriptのBlobオブジェクトのインスタンスを生成しています。
BlobオブジェクトはBLOBをJavaScriptで扱うためのオブジェクトです。
第一引数にファイルの内容の配列、第二引数にファイルの種類を指定して使います。

saveAs(blob, "file");
'file-server'というライブラリを使ってファイルを保存しています。
詳しくは割愛しますが、ブラウザ間で異なるファイル保存の処理を1行で書けます。

文字化けを修正

無事CSVをダウンロードすることができましたが、ファイルの内容が文字化けしていました。
はっきりとした原因は分かりませんでしたが、Laravel側でSJIS形式に変換していた部分をUTF-8に変換するように変更すると文字化けしなくなりました。

sample.php
// mb_convert_encoding($content, 'SJIS', 'auto');
mb_convert_encoding($content, 'UTF-8', 'auto');

HTMLのmetaタグでUTF-8に指定しているのが原因なのか?
とりあえず、これで内容も分かるCSVファイルをダウンロードできるようになりました。めでたしめでたし。

最後に

aixosを使ってファイルをダウンロードするにはひと手間いることが分かりました。
BLOBについては恥ずかしながら全然知らなかったです。

今回はLaravel側でCSVを作成しましたが、データをJSON形式で送るようにしてVue側でCSVを作成することもできそうです。

他にもこんな方法があるとか、ここ間違ってるよとかあればご指摘等よろしくお願いします!
ではでは。

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

安易にPHPのバージョンを上げたら色々ハマった(imagick編)

Qiitaに記事を書くのがこれが初めてです。

今まではハマった事、解決方法などは色々と手元のノートに書いて記録してきました。しかしそれに限界を感じ、また自分のようにハマった人がいるんだったら、その人たちへの手助けになるかと思い書き始めようと思いました。

さて、今回のお題目はタイトルの通り。
安易にPHPのバージョンを上げたら色々ハマった(imagick編)

このたびWordPressの勉強をする必要が出てきたので、apacheのhttpd-vhosts.confを弄ってAliasでディレクトリを分けて、その中にWordPress5.6のファイル展開して突っ込む。
XAMPPでずっと開発してきて、別件のプログラムでMySQLも使っていた為、データベース系の設定もちょちょっと書いたらあっさりとHelloWorldの画面が出てきて拍子抜けした。

なんか、自分が現役だった頃と全然違う…。簡単すぎ…。(10年も経てばそりゃ変わる)

WordPressの設定画面を色々と弄り倒していたところ、サイトヘルスという場所に行きついた。
どれだけ自分の環境が安全であるかを教えてくれるらしい。

そこで指摘された点はこちら。
・SSL化されてない
・PHPのバージョン低すぎ

PHPのバージョンは確かにXAMPPを突っ込んだ2018年から全然変えてない。7.2.x。指摘されて当然。XAMPPの場合、バージョンを上げるにはアンインストール→新しいのをインストールするしかないらしく、ひーこら言いながら最新のXAMPPをインストール+前の設定の引継ぎをおこなった。
参考にしたサイトはこちら。
【XAMPP】最新版のXAMPPにアップデートする際の注意と手順【Web開発】【XAMPP】 – クラベル
https://cravelweb.com/webdesign/xampp-upgrade-setup

めでたくPHP8.0.2インストール完了。

再びサイトヘルスを開くと今度はまた色々と怒られた。
・SSL化されてない
・gdがインストールされていないか無効化されている
・imagickがインストールされていないか無効化されている

gdに関してはphp.iniのextension=php_gd.dllがコメントアウトされていた為、それを外してやる事で解決。

問題はimagick。XAMPPでimagickをインストールする手順が煩雑。imagick以外にもImageMagickも入れてやる必要があるため、ファイルを落としてきてインストール。こちらは参考にしたサイトは割愛。
ImageMagickもインストールしてPathも通した。php/ext/の下にphp_imagick.dllを配置して、php.iniにphp_imagick.dllを追加。
さて再起動。

phpinfo()の中にimagickが無い。

もしかしてPHP8系に非対応?
アタリでした。

WordPressのサポートに同じ状態になった人がいらっしゃいました。
https://ja.wordpress.org/support/topic/%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E7%92%B0%E5%A2%83%EF%BC%88xampp%EF%BC%89%E3%81%A7/

こればかりは手も足も出ません。imagick側の対応を待つしかないんでしょうか。

今回の教訓。「検証しないで安易にバージョン上げると大変になる」。

今回の件で「今まで動いていたTwitterOAuthが動かなくなった」編もありますので、後日書きたいと思います。
では。

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

Fizz Buzzに対抗して世界のナベアツプログラムを紹介します。

ヨーロッパで「Fizz Buzz」という多人数で1から順に数を数えて
・3の倍数を「Fizz」
・5の倍数を「Buzz」
・3でも5でも割り切れる数は「Fizz Buzz」
・それ以外は数字
を言うゲームがあります。世界のナベアツで有名な桂三度さんの
「3の倍数、3が付く数字を言うとアホになる」
というプログラムを書きました。JavaとPHPで1~50までのプログラムです。

Java

//世界の「ナベアツ」ゲーム
//1から50までのを対象に、
// 3の倍数、数字に3がつくと、アホになる
//
//
// 新規作成日 2021/3/15
// Author 乃木坂好きのITエンジニア

package paiza;

public class Sekainonabeatu {
 public static void main(String[] args) {
  for(int i=1;i<=50;i++)

 {

   if ((i % 3 ==0) ||( i/10 == 3) || (i%10 == 3)) {

       System.out.println(i + "になるとアホになる");

   }else {

       System.out.println(i);

   }

}

}

}

PHP
//
// ナベアツプログラム
// Date 2021/03/15 (新規作成)
// Author 乃木坂46好きのITエンジニア
//
//

<?php

for($i=0;$i<100;$i++)
{

if ($i % 3 ==0 || $i / 10 ==0 || $i++ )

{

   echo $i.";でバカになる\n";

} else {

    echo $i."\n";

}

}

?>
分岐処理をマスターした方は是非挑戦してみてください。

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

【PHP入門】PHPの文字列について

はじめに

こちらは、PHP5上級試験/準上級試験まとめ の一環として、PHPの文字列について順を追って解説した記事です。
試験対象はPHP5ですが、PHP7くらいまでなら対応できるよう、汎用的な内容を心がけています。内容に間違いがある場合はご指摘ください。
なお2021年秋には PHP8技術者認定上級試験 が始まる予定ですので、最新情報は公式ページよりご確認ください。

文字列とは

文字列とは、文字通り(?)文字で構成された値のことですが、PHPで文字列を表現するには 'シングルクォート(引用符)'"ダブルクォート(二重引用符)" で文字を囲う必要があります。
例えば 1 とだけ書かれた値は数値の1を表し、'1'"1" と書かれた値は文字列としての1を表します。
(ヒアドキュメントやNowdocという機能で文字列を表現することもできますが、これについては後述します。)

数値と文字の表現
echo 1; // 1
echo '1'; // 1

// どちらも出力されるのは 1 だが、内部的には数値の 1 か、文字の 1 かが区別されている

文字列内でシングルクォートやダブルクォートを使用したい場合はどうするかと言うと、エスケープシーケンスを使用します。エスケープシーケンスとは、プログラムにとって特別な意味を持つ文字の直前に \バックスラッシュ を書くことによって、その文字のプログラム的な意味を無効化させることを言います。
ちなみにバックスラッシュという文字そのものも、他の文字をエスケープさせる特別な意味があるため、ただの文字として使用するにはバックスラッシュでバックスラッシュをエスケープする必要があります。(使用するOSによってはバックスラッシュが ¥ の記号で表される場合があります。)

バックスラッシュをエスケープシーケンスで表現
echo '\\'; // \

シングルクォートとダブルクォートでは、エスケープシーケンスの使用できる文字に違いがあります。

シングルクォート内で可能なエスケープと使用例

  • バックスラッシュ \\\
  • シングルクォート '\'

ダブルクォート内で可能なエスケープと使用例

  • バックスラッシュ \\\
  • ダブルクォート '\"
  • ドル記号 $\$
  • 改行記号 n\n
  • タブ記号 t\t

それ以外のエスエープシーケンスについてはPHPマニュアルを参照してください。
PHPマニュアル - 文字列

文字列内での変数展開について

シングルクォートやNowdoc内では変数展開(変数のパース、埋め込んだ変数の中身が展開されること)はされませんが、ダブルクォートやヒアドキュメント内では変数展開が行われます。

シングルクォートでは変数展開が行われない
$str = 'test';
echo '$str'; // $str 
ダブルクォートでは変数展開が行われる
$str = 'test';
echo "$str"; // test

ダブルクォートで囲っていても変数名のすぐ後に文字が続くと変数展開ができないので、その場合は {} で変数の部分だけを囲います。

ダブルクォートで変数展開が失敗する例
$str = 'テスト';
echo "今日は$strです。"; // [エラー] Notice: Undefined variable: strです。 in /sample/index.php on line 3
{}を使用して変数展開する例
$str = 'テスト';
echo "今日は{$str}です。"; // 今日はテストです。

PHPマニュアル - 文字列 - 変数のパース

変数展開用の {} 記号は、下記のように$記号を外に出して書いても同様の結果となります。

$str = 'テスト';
echo "今日は${str}です。"; // 今日はテストです。

ヒアドキュメント

改行されて何行にも渡るような文字列を表すには、ヒアドキュメントという機能を利用するのが便利です。
コード内で <<<EOS から始まり EOS; で終わるようなセクションがヒアドキュメントになりますが、EOS (End of String) のような終端IDは自由に命名可能です。命名規則は変数や関数と同様になります。

ヒアドキュメントの例
echo <<<EOS
<h1>タイトル</h1>
<p>段落</p>
<p>段落</p>
<p>段落</p>
EOS;

ヒアドキュメントの中で使用された変数は展開され、空白文字、改行、シングルクォート、ダブルクォートはそのまま出力されますが、最後の行末の改行だけは無視されます。

変数は展開され、最終行(2回目のテスト)の行末の改行は無視される
$str = 'テスト';
echo '<span>';
echo <<<EOS
$str
$str
EOS;
echo '<span>';
出力結果
<span>テスト
テスト<span>

終端IDはインデントしたり、同じ行にコメントを書いたりはできません。ヒアドキュメントの終わりに記述できるのは、終端IDとセミコロンだけです。

終端IDをインデントしてみる
<?php
    echo '<span>';
    echo <<<EOS
    テスト
    テスト
    EOS;
    echo '<span>';
出力結果(エラー)
Parse error: syntax error, unexpected end of file, expecting variable (T_VARIABLE) or heredoc end (T_END_HEREDOC) or ${ (T_DOLLAR_OPEN_CURLY_BRACES) or {$ (T_CURLY_OPEN) in /sample/index.php on line 8

(余談として、自分の環境で試したところPHP 5.4では終端IDをインデントするとエラーになり、PHP 7.3ではならなかったんですが、PHPの公式マニュアルでも「終端IDとセミコロンだけにしてね」と書いてあるので従っておくのが無難かなと思います。)

PHPマニュアル - 文字列 - ヒアドキュメント

Nowdoc

ヒアドキュメントに似た機能ではNowdocというものがあり、ヒアドキュメントがダブルクォーテーションで文字列を囲ったときの動きに近いとすれば、Nowdocはシングルクォーテーションで文字列を囲ったときの動きに近くなります。PHP 5.3以降ならば利用可能で、Nowdoc開始時の終端IDをシングルクォーテーションで囲うことと、変数展開がされないこと以外は基本的にヒアドキュメントと同じ動きになります。

PHPマニュアル - 文字列 - Nowdoc

Nowdocで変数の展開は行われない
$str = 'テスト';
echo '<span>';
echo <<<'EOS'
$str
$str
EOS;
echo '<span>';
出力結果
<span>$str
$str<span>

echoとprint

その他、今までのコードにも出てきていますが、文字列の出力には echo print printf などを使用します。
厳密に言えば echoprint は関数ではなく言語構造であり、引数となる後ろ側の部分を丸カッコで囲っても囲わなくても機能します。

print 'Hello world!'; // Hello world!
print('Hello world!'); // Hello world!

echo 'Hello world!'; // Hello world!
echo('Hello world!'); // Hello world!

その他の言語構造についてはPHPマニュアルを参照してください。

PHPマニュアル - キーワードのリスト

echo はカンマで区切って複数の値を引数として与えられますが、print ではカンマ区切りの複数の値を与えることはできません。
また、echo を使いカンマ区切りで複数の値を出力するときは、丸カッコで引数を囲うことはできません。
(複数の値を出力するときは、カンマを使わずドットで連結することの方が多いかもしれませんが。)

printにカンマ区切りで複数の値を与える
print 'Hello ', 'world!'; // [エラー] Parse error: syntax error, unexpected ',' in /sample/index.php on line 2
print('Hello ', 'world!'); // [エラー] Parse error: syntax error, unexpected ',' in /sample/index.php on line 2
echoにカンマ区切りで複数の値を与える
echo 'Hello ', 'world!'; // Hello world!
echo('Hello ', 'world!'); // [エラー] Parse error: syntax error, unexpected ',' in /sample/index.php on line 2

echoprint の違いとして他には、print には返り値があり、常に数値の 1 を返すため式の一部として使うことができますが、 echo には返り値がありません。

echoを式の一部として使用する
if(echo 'echo'){
    echo 'OK';
}
// [エラー] Parse error: syntax error, unexpected 'echo' (T_ECHO) in /sample/index.php on line 2

true ? echo 'OK' : echo 'NG';
// [エラー] Parse error: syntax error, unexpected 'echo' (T_ECHO) in /sample/index.php on line 3
printを式の一部として使用する
if(print 'print'){
    print 'OK';
}
// printOK

true ? print 'OK' : print 'NG';
// OK

echoprint の違いについては別な記事でもまとめているので、興味のある方は読んでみてください。

PHP : echoとprintの違い

printf

printf() は、言語構造ではなく組み込み関数で、書式文字列を使って文字列をフォーマットすることができます。
書式文字列は 置換マーク % で始まり、修飾子 02 -5 などは必要に応じて付け、型指定子 d s などで終わります。

printfの構成
printf('[置換マーク][修飾子][型指定子]');
置換マーク、修飾子、型指定子の例
printf('%d',9); // 9 // dは10進数のdecimalを表す型指定子
printf('%2d',9); //  9 // 9の直前に空白が置かれて2桁になっている
printf('%02d',9); // 09 // 2桁で出力し、足りない桁数は0で埋める
printf('%f', 3.14); // 3.140000 // fは浮動小数点数のfloatを表す型指定子
printf('%1.2f', 3.14); // 3.14 // 整数1桁.小数2桁
printf('%s','Hello world!'); // Hello world! // sは文字列のstringを表す型指定子
printf('%12s','world!'); //       world! // 文字列の前に空白が置かれ12桁になっている
printf('%-12s','Hello'); // Hello        // 文字列の後に空白が置かれ12桁になっている
printf('%d%s%d%s', 1, '年', 1, '組'); // 1年1組 // 左から順に対応する引数を出力
printf('%%'); // % // %文字を出力

より詳しい printf() の仕様はマニュアルを確認してください。

PHPマニュアル - printf — フォーマット済みの文字列を出力する

print_r と var_dump

print_r()var_dump() を使用すると、変数の値などをデバッグ用にわかりやすく出力してくれます。
パッと見で読みやすいのは print_r() かもしれませんが、var_dump() の方はデータ型や null false もきちんと文字列で出力してくれます。
可読性優先だったら print_r()、より詳細に情報を出力したいときは var_dump() を使うと良いでしょう。

$array = array( null, false, 'string', 1 );
var_dump($array);
print_r($array);
print_rによる出力結果
Array ( [0] => [1] => [2] => string [3] => 1 )
var_dumpによる出力結果
array(4) { [0]=> NULL [1]=> bool(false) [2]=> string(6) "string" [3]=> int(1) }

strlenとmb_strlen

strlen()mb_strlen() では、文字列の文字数を調べることができます。
日本語の文字はマルチバイト文字といって通常の半角英数字とは異なるので、マルチバイト文字の文字数を調べたいときは mb_strlen() の方を使います。

strlenの場合
echo strlen('Hello!'); // 6
echo strlen('こんにちは!'); // 18 // strlenで日本語の文字数は数えられない
mb_strlenの場合
echo mb_strlen('Hello!'); // 6 // mb_strlenでは半角英数も対応する
echo mb_strlen('こんにちは!'); // 6

PHPマニュアル - strlen — 文字列の長さを得る
PHPマニュアル - mb_strlen — 文字列の長さを得る

マニュアルにもありますが、strlen() というのは厳密には文字のバイト数(大きさ、容量のようなもの)を調べる組み込み関数です。
半角英数字は1文字1バイトなので バイト数=文字数 と読み替えても不都合はあまりありませんが、日本語のようなマルチバイト文字というのは1文字が3バイトとか4バイトしたりすることもあるので、strlen() でバイト数を調べると文字数よりも大きな数値が出てしまうということになります。

trim / ltrim / rtrim

trim() ltrim() rtrim() のそれぞれの関数は、文字列から特定の文字を削除してくれます。
trim() は文字列の両端から削除、ltrim() は文字列の左側(left)のみ削除、rtrim() は文字列の右側(right)のみ削除します。
第一引数には削除処理を適用したい文字列、第二引数には削除したい文字を指定することができます。第二引数は省略することも可能で、省略した場合は 空白, タブ, リターン, 改行, NULバイト, 垂直タブ を削除します。

trimで第二引数を省略した場合
echo trim(' Hello '); // Hello // 文字列の左右両側から空白文字が除去される

ちなみに削除する文字を指定する際、trim('Hello','He') などとした場合は He という文字列を削除するのではなく、He の文字を削除しようとします。trim('Hello','Hel') などとした場合は Hel という文字列ではなく Hel の文字を削除しようとするので、次の2つ目の例では l が2文字とも削除されて o だけが残っています。

trimで第二引数を指定した場合
echo trim('Hello','He'); // llo
echo trim('Hello','Hel'); // o

PHPマニュアル - trim — 文字列の先頭および末尾にあるホワイトスペースを取り除く
PHPマニュアル - ltrim — 文字列の最初から空白 (もしくはその他の文字) を取り除く
PHPマニュアル - rtrim — 文字列の最後から空白 (もしくはその他の文字) を取り除く

strtolower / strtoupper / ucfirst / ucwords

大文字・小文字を変換するには、strtolower() strtoupper() ucfirst() ucwords() などの関数が便利です。

strtolower() は、引数に与えられた文字列の中でアルファベットの部分をすべて小文字にして返します。

echo strtolower('Hello World!'); // hello world!

strtoupper() は、引数に与えられた文字列の中でアルファベットの部分をすべて大文字にして返します。

echo strtoupper('Hello World!'); // HELLO WORLD!

ucfirst() は、引数に与えられた文字列の最初の文字がアルファベットの場合に、それを大文字にします。

echo ucfirst('hello world!'); // Hello world!
echo ucfirst('こんにちは world!'); // こんにちは world! // 1文字目がアルファベットでないため変化なし

ucwords() は、引数に与えられた文字列のアルファベットにおいて、各単語の1文字目を大文字にします。各単語の区切りはデフォルトだと空白文字や改行などですが、第二引数に区切り文字を指定すると、指定の区切り文字の直後が大文字に変換されるようになります。

echo ucwords('hello world!'); // Hello World! // 半角スペースを区切りと認識
echo ucwords("hello \nworld!"); // Hello World! // 改行(\n)を区切りと認識
echo ucwords('hello world!', 'l'); // HelLo worlD! // l(エル)を区切りと認識

PHPマニュアル - strtolower — 文字列を小文字にする
PHPマニュアル - strtoupper — 文字列を大文字にする
PHPマニュアル - ucfirst — 文字列の最初の文字を大文字にする
PHPマニュアル - ucwords — 文字列の各単語の最初の文字を大文字にする

substr / substr

参考URL

PHPの変数展開は、{$variable}構文を使うのがいいみたいです
PHP 文字列リテラルにおける変数展開ノ全テ
浮動小数点って何?

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

PHPでQRコードを作ってみた

今回は動的にサイトのURLを作成したかったので、PHPでQRコードを作成したみました。

結論から言いますと、全く難しくないです!!

読み取りもしっかりと行えます!

なので、肩の力を抜いてご覧ください。

それでは説明していきます!

まずはQRコードを作成するためのライブラリをダウンロードしてください。

compose require endroid/qr-code

後は下記の記述を参考にQRコードを作成します。

今回はコントローラーでQRコードを作成します。

<?php

namespace App\Http\Controllers;

use App\Models\Qr;
use Illuminate\Http\Request;
use Endroid\QrCode\Color\Color;
use Endroid\QrCode\Encoding\Encoding;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelLow;
use Endroid\QrCode\QrCode;
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin;
use Endroid\QrCode\Writer\PngWriter;

class QrCodeController extends Controller
{
    public function generate(Request $request) {
        $writer = new PngWriter();

        $origin_URL = 'https://www.example.com';

        // Create QR code
        $qrCode = QrCode::create($origin_URL)
            ->setEncoding(new Encoding('UTF-8'))
            ->setErrorCorrectionLevel(new ErrorCorrectionLevelLow())
            ->setSize(300)
            ->setMargin(10)
            ->setRoundBlockSizeMode(new RoundBlockSizeModeMargin())
            ->setForegroundColor(new Color(0, 0, 0))
            ->setBackgroundColor(new Color(255, 255, 255));

        // QRコードの出力
        $result = $writer->write($qrCode);
        header('Content-Type: '.$result->getMimeType());
        echo $result->getString();


        // // QRコードの画像ファイルを保存
        // $result->saveToFile(__DIR__.'/qrcode.png');

        // // QRコードのURL
        // $dataUri = $result->getDataUri();
    }
}

このようにしてQRコードを作成することができます!

実際にコントローラーを適用しているURLに移動すると下記のように表示されます。

無題.png

めちゃめちゃ簡単!

以上、「PHPでQRコードを作ってみた」でした!

Thank you for reading

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

【個人開発】作ったサイトがクソつまらないのでアドバイスをください

入力された金額を青春18きっぷ何回分かに変換するサイトです。
初めてプログラミング(PHP)を学習し、作ってみました。

なんでつくったか

僕自身、鉄道旅行が好きですがお金がないので行きつくのは青春18きっぷ。
そんな旅行を繰り返していると、何かものを買おうとするときに
「青春18きっぷ○○回分だから~まで旅行できるな」と考えてしまいます。
そんな18きっぱーの脳内を表現しようと思い、このサイトをつくりました。

アドバイスをください

数字を入力して青春18きっぷ何回分かを表示するだけというのはとてもつまらないし、使ってもらえない。
具体的な使用用途、利用場面がないというのが一番の問題だと思っています。
ですが、具体的にこんな機能があればというのがわかりません...

こんな機能があったらどうだろう?
こんなサイトのデザインがいいんじゃない?
などいろいろなアドバイスをコメントで頂けたら嬉しいです。

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

Phalcon4にバージョンアップしたあとにDBの更新系が動かなくなったら

過去にPhalcon4にバージョンアップしたときにModel周りの更新がうまく行かなくなったことがありました。
この問題に当たってから時間が経ってしまったので実際の動作を忘れてしまいましたがこの辺気にすると良いよ程度で書いておきます。

概要

Phalconを4にバージョンアップした。
参照系は動くけど、更新がなんかされない。saveメソッドはtrueが返ってくる。

対応1 - ModelのkeepSnapshotsまたはuseDynamicUpdateを有効にする。

Phalcon3系まではModelのデータを取得したあとに何かデータが更新されれば、過去のデータと照らし合わせて更新されるような形が取られてました。

しかし4系からはkeepSnapshotsというオプションを有効にしないと過去のスナップショットが保持されず、更新がされません。

class User extends Model
{
    public function initialize()
    {
        $this->keepSnapshots(true);
    }
}

useDynamicUpdateでもこのkeepSnapshotsのオプションは有効になるので、以前のバージョンからuseDynamicUpdateを利用している場合は影響はないでしょう。

対応2 - リレーションのreusableオプションを有効にする

リレーション先のデータを更新しようとする際にはreusableオプションを有効にしてないとデータの更新がされません。

$this->hasMany('id', UserPost::class, 'user_id', [
  'alias' => 'posts',
  'reusable'   => true, // ←これ
]);

ついでに、resusableを有効にしてないと毎回リレーション先データを取ってきてしまうのでデフォルトで有効にしておいてほしいですね。。。

また、3系では以下のようにデータが更新できました。

$user = User::find(1);
$user->userCard->token = $token;
$user->save();

しかし、4.0や4.1.0では更新されません。
この件に関しては以下のissueを立てました。
内部のコードを見るとわかりますが、リレーション先がdirtyかどうかをチェックしてないようです。

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

PHPのPDOでMySQLに接続する

環境

・macOS BigSur (11.2.2)
・PHP (7.4.12)
・MAMP (6.3)
・MySQL (5.7.32)

前提条件

・MAMPのphpMyAdminでデータベース・テーブルを作成してあること
・データベースに接続するためのユーザーを作成してあること

PDOとは

・PHPからMySQLに接続するために必要なオブジェクトのこと
※PDOクラスのインスタンスを生成することにより、データベースサーバーとの接続が確率される

DB接続手順

//事前準備
$dsn = 'mysql:host=localhost;dbname=データベース名;charset=utf8';
$user = 'ユーザー名';
$pass = 'パスワード';

変数に対象の「データベース情報」「DB接続するユーザー情報」を設定

//DB接続
$dbh = new PDO($dsn,$user,$pass);

設定した変数を引数に入れて、PDOのオブジェクト生成

データを取得する

指定した列名のデータを全て取り出す例

※上記で生成したPDOオブジェクトを使用する

//SELECT文
$select_sql = 'SELECT * FROM テーブル名';
$stmt = $dbh->query($select_sql);
foreach($stmt as $value){
  echo $value['カラム名'];
}

データを登録する

入力フォームなどで入力された値を、プリペアドステートメントを使用してDBに登録する例

※プリペアドステートメントとは、SQL文を最初に用意しておいて、その後はクエリ内のパラメータの値だけを変更してクエリを実行できる機能のこと

//INSERT文
$insert_sql = 'INSERT INTO テーブル名(カラム名) VALUES(:name)';
$stmt = $dbh->prepare($insert_sql);
$params = array(':name'=>$_POST['フォームに入力された値']);
$stmt->execute($params);

カラム名にデータ登録したいカラム名を指定する
VALUESを「:name」として抽象化し、後に$_POSTで値を設定する(※nameの部分は任意の文字で良い)
prepareメソッドでSQL文を実行する準備をする
executeメソッドでプリペアドステーメントを実行する

※正式な値が入るまで一時的に場所を確保しておくために入れておく値のことをプレースホルダという
(今回だと「:name」の部分がプレースホルダ)
※プレースホルダに値を割り当てることをバインドという

データを削除する

指定された番号のIDを持つDBの値を削除する例

//DELETE文
$delete_sql = 'DELETE FROM テーブル名 WHERE idが入ってるカラム名 = :id';
$stmt = $dbh->prepare($delete_sql);
$params = array('id'=>$_POST['指定された番号']);
$stmt->execute($params);

上記と同様、プリペアドステーメントを使用

データを変更する

指定された番号のIDを持つデータを対象に、指定されたカラム名の値を更新する例

//UPDATE文
$update_sql = 'UPDATE テーブル名 SET 更新するデータのカラム名 = :name WHERE 更新対象とするidが入ってるカラム名 = :id';
$stmt = $dbh->prepare($update_sql);
$params = array('name'=>$_POST['更新するデータ'],'id'=>$_POST['指定された番号']);
$stmt->execute($params);

上記と同様、プリペアドステーメントを使用

try~catchを使ったエラーハンドリング(例外処理)

DB接続の成功/失敗を出力する例

try{
  $dsn = 'mysql:host=localhost;dbname=データベース名;charset=utf8';
 $user = 'ユーザー名';
  $pass = 'パスワード';
  $dbh = new PDO($dsn,$user,$pass);
  echo 'DB接続成功'
}catch(PDOException $e) {
  echo 'DB接続エラー';
}

(例外処理についての補足)
プログラム実行中に発生したエラーのことを例外という
例外が発生すると、プログラムが異常終了する
システム的に異常終了はさせたくないから、例外を検知して、例外が発生した場合に行う処理を用意する
この一連の処理のことを例外処理という
結果、エラーは発生しているが、プログラム自体は正常に動作したという状況を作ることができる
DBの接続が失敗したとき、プログラムが異常終了する
システム的に異常終了はさせたくないから、 DBの接続処理に対してエラーハンドリングする必要がある

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

PHP×MAMPでDB接続を行う

環境

・macOS BigSur (11.2.2)
・PHP (7.4.12)
・MAMP (6.3)
・MySQL (5.7.32)

前提条件

・MAMPのphpMyAdminでデータベース・テーブルを作成してあること
・データベースに接続するためのユーザーを作成してあること

PDOでDB接続

※PDOとは、MAMPでPHPからMySQLに接続するために必要なオブジェクトのこと

//事前準備
$dsn = 'mysql:host=localhost;dbname=データベース名;charset=utf8';
$user = 'ユーザー名';
$pass = 'パスワード';

変数に対象の「データベース情報」「DB接続するユーザー情報」を設定

//DB接続
$dbh = new PDO($dsn,$user,$pass);

設定した変数を引数に入れて、PDOのオブジェクト生成

データを取得する

生成したオブジェクトを使用してDBからデータを取り出す

※指定した列名のデータを全て取り出す例

//SELECT文
$select_sql = 'SELECT * FROM テーブル名';
$stmt = $dbh->query($select_sql);
foreach($stmt as $value){
  echo $value['列名'];
}

データを登録する

入力フォームなどで入力された値を、プリペアドステートメントを使用してDBに登録する例

※プリペアドステートメントとは、SQL文の値をいつでも変更できるように、変更する部分のみ変数のようにした命令文を作る仕組みのこと

//INSERT文
$insert_sql = 'INSERT INTO テーブル名(列名A) VALUES(:name)';
$stmt = $dbh->prepare($insert_sql);
$params = array(':name'=>$_POST['フォームに入力された値']);
$stmt->execute($params);

列名Aにデータ登録したい列名を指定する
VALUESを「:name」として抽象化し、後に$_POSTで値を設定する(※nameの部分は任意の文字で良い)
prepareメソッドでSQL文を実行する準備をする
executeメソッドでプリペアドステーメントを実行する

※「:name」と変化する値を確保することをプレースホルダという
※「:name」の部分に値を割り当てることをバインドという

データを削除する

指定された番号のIDを持つDBの値を削除する例

//DELETE文
$delete_sql = 'DELETE FROM テーブル名 WHERE idが入ってる列名 = :id';
$stmt = $dbh->prepare($delete_sql);
$params = array('id'=>$_POST['指定された番号']);
$stmt->execute($params);

上記と同様、プリペアドステーメントを使用

データを変更する

指定された番号のIDを持つデータを対象に、指定された列名の値を更新する

//UPDATE文
$update_sql = 'UPDATE テーブル名 SET 更新するデータの列名 = :name WHERE 更新対象とするidが入ってる列名 = :id';
$stmt = $dbh->prepare($update_sql);
$params = array('name'=>$_POST['更新するデータ'],'id'=>$_POST['指定された番号']);
$stmt->execute($params);

上記と同様、プリペアドステーメントを使用

try~catchを使ったエラーハンドリング(例外処理)

DB接続の成功/失敗を出力する例

※例外処理とは、エラーが発生した際の処理を設定しておくこと
(try~catchの間でエラーが発生した時、catchの中の処理が行われる)

try{
  $dsn = 'mysql:host=localhost;dbname=データベース名;charset=utf8';
 $user = 'ユーザー名';
  $pass = 'パスワード';
  $dbh = new PDO($dsn,$user,$pass);
  echo 'DB接続成功'
}catch(PDOException $e) {
  echo 'DB接続エラー';
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP×MAMPでDB操作する

環境

・macOS BigSur (11.2.2)
・PHP (7.4.12)
・MAMP (6.3)
・MySQL (5.7.32)

前提条件

・MAMPのphpMyAdminでデータベース・テーブルを作成してあること
・データベースに接続するためのユーザーを作成してあること

PDOでDB接続

※PDOとは、MAMPでPHPからMySQLに接続するために必要なオブジェクトのこと

//事前準備
$dsn = 'mysql:host=localhost;dbname=データベース名;charset=utf8';
$user = 'ユーザー名';
$pass = 'パスワード';

変数に対象の「データベース情報」「DB接続するユーザー情報」を設定

//DB接続
$dbh = new PDO($dsn,$user,$pass);

設定した変数を引数に入れて、PDOのオブジェクト生成

データを取得する

指定した列名のデータを全て取り出す例
※上記で生成したオブジェクトを使用する

//SELECT文
$select_sql = 'SELECT * FROM テーブル名';
$stmt = $dbh->query($select_sql);
foreach($stmt as $value){
  echo $value['列名'];
}

データを登録する

入力フォームなどで入力された値を、プリペアドステートメントを使用してDBに登録する例

※プリペアドステートメントとは、SQL文の値をいつでも変更できるように、変更する部分のみ変数のようにした命令文を作る仕組みのこと

//INSERT文
$insert_sql = 'INSERT INTO テーブル名(列名A) VALUES(:name)';
$stmt = $dbh->prepare($insert_sql);
$params = array(':name'=>$_POST['フォームに入力された値']);
$stmt->execute($params);

列名Aにデータ登録したい列名を指定する
VALUESを「:name」として抽象化し、後に$_POSTで値を設定する(※nameの部分は任意の文字で良い)
prepareメソッドでSQL文を実行する準備をする
executeメソッドでプリペアドステーメントを実行する

※「:name」と変化する値を確保することをプレースホルダという
※「:name」の部分に値を割り当てることをバインドという

データを削除する

指定された番号のIDを持つDBの値を削除する例

//DELETE文
$delete_sql = 'DELETE FROM テーブル名 WHERE idが入ってる列名 = :id';
$stmt = $dbh->prepare($delete_sql);
$params = array('id'=>$_POST['指定された番号']);
$stmt->execute($params);

上記と同様、プリペアドステーメントを使用

データを変更する

指定された番号のIDを持つデータを対象に、指定された列名の値を更新する

//UPDATE文
$update_sql = 'UPDATE テーブル名 SET 更新するデータの列名 = :name WHERE 更新対象とするidが入ってる列名 = :id';
$stmt = $dbh->prepare($update_sql);
$params = array('name'=>$_POST['更新するデータ'],'id'=>$_POST['指定された番号']);
$stmt->execute($params);

上記と同様、プリペアドステーメントを使用

try~catchを使ったエラーハンドリング(例外処理)

DB接続の成功/失敗を出力する例

※例外処理とは、エラーが発生した際の処理を設定しておくこと
(try~catchの間でエラーが発生した時、catchの中の処理が行われる)

try{
  $dsn = 'mysql:host=localhost;dbname=データベース名;charset=utf8';
 $user = 'ユーザー名';
  $pass = 'パスワード';
  $dbh = new PDO($dsn,$user,$pass);
  echo 'DB接続成功'
}catch(PDOException $e) {
  echo 'DB接続エラー';
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelの学習をしてウェブアプリを作ったのでやったことまとめ

制作アプリについて

アプリ概要

インスタ風の文字投稿専用SNS
スクリーンショット 2021-03-15 2.52.52.png

会員登録、ログイン、プロフィール編集
投稿時画像加工、投稿・編集・削除

現在の不具合状況について
・アップロード画像が2MBを超えるものの投稿ができない
・投稿した画像が横向きに反転してしまう

作った経緯と目的

・転職時に必要なため

・自分自身のあったらいいなをアプリ化

1つ目。自分自身何かエンジニアとしての能力証明ができる術を持っていなかったので、転職時のポートフォリオとしてのスキル証明用になればと思い制作
2つ目。自分が必要としていることは誰かもまた必要としているのかなと思い「自分自身のあったらいいな」というものを実現させた。
具体的には、
「○○って字を綺麗に書きたいけど検索しても自分の気に入る字体がみつからない」
「参考になる字体を検索して探したい」

という課題の解消させた。
イメージとしては、お手本の字をただ真似て書くのではなく、自分の書きたい文字について探してからお手本にして書く。
プログラミング学習するときに、課題で出されたものを作るのではなく、自分の作りたいものを調べながら作るという感じです。

開発期間

Laravel の基礎学習期間二週間とアプリ開発に1ヶ月ほど

ER図

カテゴリー機能はまだ追加していないので今の所お飾りです。
スクリーンショット 2021-03-11 8.16.22.png

開発環境

・言語:PHP7, Bootstrap, HTML, CSS(Sass)
・フレームワーク:Laravel8
・DB:SQLite→postgreSQL
・環境:ローカル環境(MacOS), VSCode
・インフラ:AWS S3, heroku

主な機能

・CRUD
・会員登録、ログイン、ログアウト
・画像アップロード
・DBリレーション
・検索機能
・マイページ
・ペジネーション
・画像加工
・バリデーション

追加できたらいいもの

・2枚以上の画像投稿
・タグ機能
・お気に入り
・JavaScriptでのデザイン、操作
勉強中であり多言語の知識が必要で時間がかかりそうなため一時保留
画像加工は強制的にしているので何かしらの形で修正したい

感想

簡単に手書きで欲しい機能、デザイン、DB設計などを考えてから始めたものの
途中途中であれもいいかもこれもいいかもなんてことを思いよく脱線したので、一番初めにしっかりと考えてからやれたらもっと早く作り終えたと思う。
でもその中で何が必要で何があったら便利で何がいらない機能なのかなどひとつひとつ客観的に物事を考えて自分自身で試行錯誤するところは楽しかったし面白かったです。
エンジニアの挫折率が高い所以の一つに、エラーや不具合があると思うけど、この模索している時がなんだかんだ一番面白く、丸1日動かなかったエラーが解消されたときはとても気持ちが良かったです。
GitHubにコードあげ忘れることが多かったのでもう少し意識してできたら良かった。

その他メモ

フロントエンド側のデザインUI/IXに関してはあまり自分の希望する分野ではないので最低限に収めてバックグラウンドの方に重きを置いていたので、フロントエンド側にももう少し手を加えて改善したい。
GitHubの使い方、DB操作、他言語、インフラなど知って行くたびに自分の知らないことが多いのでこれからも少しずつ学習していきたい。

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

PHPをherokuでデプロイする!(3)データベースのインポート

はじめに

PHPをherokuでデプロイする方法、今回はローカル環境で作ったデータベースの引継ぎとしてインポートする方法を書いていきます!

データベースをインポートするための準備

まずはローカル環境で作ったデータベースを引き継ぐ形でデータベースを作成していきます。前回でcleardbのアドオンを用いてMySQLが利用できるようになっていると思います。PHPではPHPMyAdminでデータベースの作成や編集、データ内容の確認ができますが、あくまでこれはローカル環境のみとなっています。herokuでデータベースをインポートするために便利なのが、「MySQL Workbench」です。

MySQL Workbenchのインストール

MySQL WorkbenchはMySQLが公式に提供しているアプリで、GUIで操作しやすいのが特徴です。ダウンロードはこちらです。
https://www.mysql.com/jp/products/workbench/
「ダウンロードはこちら」ボタンを押し自分のOSを選んでダウンロードし、インストールを進めましょう。ダウンロードする前にユーザー登録を求められますが、「No thanks, just start my download.」が下に表示されているのでそれを押すとユーザー登録なしにダウンロードできます。

なお、最新バージョンだとインストールしたあとmacでは動作しないことがあったようです。その場合はArchivesボタンを押して、8.0.19をダウンロード、インストールすると確実に動作します。インストール後表示にしたがってアイコンをドラッグするとmacであればApplicationsフォルダーにコピーされます。

MySQL Workbenchの設定

起動させると
Work1.png
こんな画面になります。MySQLConnectionsの隣にある+のボタンを押すとデータベースの作成ができます。

work2.png
ここで入力していくのはherokuのデータベースの情報です。(1)の中でも紹介した、herokuのデータベース情報を元に入力しましょう。herokuのデータベース情報はターミナルでデプロイしたいアプリにアクセスして以下を入力してください。

heroku config | grep CLEARDB_DATABASE_URL

ここで表示されるのは、CLEARDB_DATABASE_URL: mysql://ユーザー名:パスワード@ホスト名/データベース名?reconnect=trueとなります。
これに基づいて先ほどの画像の内容を入れていきましょう。

まず、「connection name」ですがこれはこのデータベースを管理する上での名前なので、好きに名付けて構いません。「conecction Method」はそのままの設定で進めましょう。
Hostnameは、先ほどherokuconfigで確認したホスト名を、Usernameも確認したユーザー名を入れましょう。Portはそのままで構いません。
そしてパスワードは「Store in Key chain」ボタンをクリックすると入力できます。入力内容が確認できないので間違いないように気をつけてください。
「Default Schema」にはデータベース名を入れましょう。

全て入れたらTest conecctionボタンを押してみましょう。そうすると問題なければ「Successfully made the MySQL connection」と表示されます。もし問題がある場合は入力内容に間違いがないかみていきましょう。これで接続できていれば、データベースをインポートする準備ができています!

データベースのエクスポート

次に、インポートするためにPHPMyAdminからデータをエクスポートします。
PHPMyAdminにアクセスし、エクスポートボタンを押して、エクスポート方法で詳細を押すと、データベースの選択ができるので、エクスポートしたいデータベースを選びましょう。
出力は必ず「出力をファイルに保存する」を選びましょう。これでないとインポートできません。その他はそのままで問題ないので、実行を押すとファイルがダウンロードされます。

データベースのインポート

それでは、いよいよインポートの作業に入ります!MySQL workbenchを再び開くと、先ほど追加したデータベースが表示されていると思います。ボタンを押すと少し接続の待ち時間があり、その後画面が開きます。

Work3.png
このような画面になったら、Data Import/Restoreを開きましょう。

work4.png
そうするとこんな感じの画面になるので、「Import from Self-Contained File」を選択し、先ほどエクスポートしたファイルを選びましょう。また「Default Target Schema」ではherokuのデータベース名を選択しましょう。
そして「Import progress」を選択し、「Start Import」を押せばインポート完了です!

もしうまくいかない時は・・・

ちなみに、もしインポートできない場合は、データベース名をコメントアウトし、テーブル名をのみを残すような形で編集するとうまくいくことがあります!ちなみにエクスポートしたデータベースは右クリックで「このアプリケーションから開く」を選択し、VScodeなどプログラミング用のアプリケーションを開けば内容を見ることができます。

次回はいよいよ最終章として、デプロイに必要なcomposerのインストール、そしてデプロイの方法について書いていきます!

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