20210405のPHPに関する記事は19件です。

【Laravel】ちゃんとフォームリクエストでバリデーションしてね

*この記事は初心者エンジニア(実務経験あり)が書いている記事です。 こんにちは @erika_chan(qiita) です。 今年度は、たくさんアウトプット出そうと思ってqiitaを始めてみました!! つよつよな人が周りにそんないなくて試行錯誤でやってます 結論 バリデーションしてないWebサービスは危ない! 今はCMSを主に開発していますがもちろんテストはしてないしバリデーションすらしてないっていう悲しい現状だったので、自分が担当するところから積極的に書いていこうということでチョロチョロっと学んでみました! バリデーションのすすめ まずバリデーションはアプリケーションのフォームからデータベースに入れる処理の際に入力され送られてきたデータをチェックして、そのデータが本当にデータベースに入れていいのか判断することです! これがないと本来メールアドレスが入って欲しいカラムに"間抜けなアプリ"のような文字列が簡単に入ってしまうためやるべき処理なのです! ではlaravelではどうやってやるの!という話ですね。 validateメソッドを使う! validateメソッドは連想配列にしたルールを引数に入れるだけで簡単にバリデーションしてくれます。 ArticleController.php /** * 新記事データの保存 * * @param Request $request * @return Response */ public function store(Request $request) { $validatedData = $request->validate([ 'title' => 'required|unique:posts|max:255', 'body' => 'required', ]); } 今回の場合は$requestで受け取ったデータのtitle, bodyが空じゃないかつtitleが255文字以下でデータベースにそのtitleが存在しないことが確認できたらそのまま通ってくれるのです! unique:posts これだけで既存のデータまでも見れるという超便利で優しいlaravelって最高ですね。 フォームリクエストバリデーションを使う これが今回のタイトルにもあるイチオシのばばばバリデーションです! このやり方だとファイルで分かれているので使い回しもできるしコントローラが簡潔に済むのでおすすめです まず以下のコマンドを叩きます。 php artisan make:request StoreArticlePost このコマンドを叩くとHttp/Requestディレクトリ配下にStoreArticlePost.phpというファイルが完成します! それがこちら↓ Http/Request/StoreArticlePost.php /** * ユーザーがこのリクエストの権限を持っているかを判断する * * @return bool */ public function authorize() *1 { // 認証系が必要な場合はfalseにしましょう return true; } /** * リクエストに適用するバリデーションルールを取得 * * @return array */ public function rules() *2 { return [ // titleが存在する、データベースにそのtitleが存在しない、255文字以下 'title' => 'required|unique:posts|max:255', // bodyが存在する 'body' => 'required', ]; } ArticleController.php /** * 新記事データの保存 * * @param StoreArticlePost $request * @return Response */ public function store(StoreArticlePost $request) { // validateメソッドを書かなくても↑だけでok return $response; } ここでは*1に認証についてのロジックがかけます。ただ他のところに書いていたり、そもそも認証する部分ではない場合が多かったりするので基本trueにしておきます。例えば特定のuserしか入力できないデータなのであればパスワードチェックするロジックなどをかけたりします。もしその認証に通っていない場合(falseを返す)は自動的に403を返してくれます! *2では上のvalidationメソッドと同じく既存のルールを用いて各リクエストの中身をチェックして問題なかったら通り、ダメだったら何がダメだったのかを教えてくれるのです。 といった感じでバリデーションがなくても動くアプリケーションはできるんです。 けどやっぱり安全かつどんなデータを入れる場所なのかをバリデーションでわかっちゃうって結構初心者にも優しいと思うんですよね。 web系エンジニアに転職する方はぜひ使ってみてください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google Maps JavaScript APIを使ってWordPressページにルートマップを埋め込む(WordPress編)

はじめに で書いたGoogle Maps Javascript Apiについて、WordPress側でやったことを書きます。 この記事ではHTMLのformにhidden属性のinputタグ設定し、idとvalueでキーバリュー型の情報を格納しています。 こんな感じ <form id=""> <input type="hidden" id="count" value="4" /> <input type="hidden" id="p0-label" value="Start" /> <input type="hidden" id="p0-title" value="Kamakura Station" /> <input type="hidden" id="p0-latlng" value="35.319264, 139.550004" /> <input type="hidden" id="p0-description" value="Tour starts from here" /> <input type="hidden" id="p1-label" value="1" /> <input type="hidden" id="p1-title" value="Tsurugaoka Hachimangu Shrine" /> <input type="hidden" id="p1-latlng" value="35.326074, 139.556485" /> <input type="hidden" id="p1-description" value="Built as a tutelary shrine for the Genji family in 1191, the most gorgeous shrine in Kamakura attracts millions of visitors for its history, culture, architecture and various traditional events." /> <input type="hidden" id="p2-label" value="2" /> <input type="hidden" id="p2-title" value="Hase-dera Temple" /> <input type="hidden" id="p2-latlng" value="35.312416, 139.533042" /> <input type="hidden" id="p2-description" value="This legendary old temple boasts of its large wooden gilded statue of Eleven-faced Goddess of Mercy, a beautiful Japanese-style garden and the nice view of the ocean from a terrace." /> <input type="hidden" id="p3-label" value="3" /> <input type="hidden" id="p3-title" value="the Great Buddha" /> <input type="hidden" id="p3-latlng" value="35.315858, 139.535371" /> <input type="hidden" id="p3-description" value="The Great Buddha, a National Treasure and a Symbol of Kamakura, was cast in bronze in the middle of the 13th C. The hollow interior of the statue is open to the public." /> </form> WordPressはphpとMySqlで動いているので、MySqlにルート設定用のテーブルをもたせ、functions.phpでデータを呼び出してformを生成しています。 また、使う際にはWordPressのショートコードを使うことでWordPressのPage作成時に簡単に地図が埋め込めるようにしました。 DataBase wp2_course、wp2_route、wp2_placeの3つのテーブルを使っています。プレサフィックスの"wp2_"はWordPress用のテーブルがすべてこの形式だったので真似しただけで、理由はないんですが、今思うと別に真似しなくても良かったような。 wp2_course(コース設定用テーブル) 物理名 データ型 NULL 既定値 説明 id text no なし コース識別コード。ショートコードでどのルート表示するか指定します。 zoom int(11) yes NULL 地図の初期表示縮尺 travelmode varchar(10) yes NULL WALKING:徒歩(既定値)DRIVING:車BICYCLING:自転車TRANSIT:公共交通機関 maptypeid varchar(10) yes NULL ROADMAP:道路や建物などが表示される地図(既定値)SATELLITE:衛星写真HYBRID:ハイブリッドTERRAIN:地形情報地図 実際にはid以外ほぼNULLで使っています。 こんな感じ id zoom travelmode maptypeid K1 NULL NULL NULL YM1 NULL NULL NULL wp2_place(地点テーブル) 物理名 データ型 NULL 既定値 説明 name varchar(50) no なし 地点の物理名 title varchar(50) no なし Markerクリック時のポップアップウィンドウのタイトル latlang geometry no なし 緯度経度 description varchar(400) no なし Markerクリック時のポップアップウィンドウに表示する説明文 例: name title latlng description KAMAKURA_STATION Kamakura Station 'POINT(35.319264 139.550004)',0 Tour starts from here. HACHIMAN_GU Tsurugaoka Hachimangu Shrine 'POINT(35.326074 139.556485)',0 Built as a tutelary shrine for the Genji family in 1191. wp2_route(経路テーブル) 物理名 データ型 NULL 既定値 説明 index int(11) no なし インデックス(Auto_Increment) id varchar(5) no なし コースid name varchar(50) no なし 地点の物理名 order int(11) no なし 経路の順序 label varchar(10) no なし Markerに表示するラベル。"-b-"とすると経路を分割する marker tinyint(1) no 1 1:マップ上にMarker生成する、0:Marker生成しない skip tinyint(1) no 0 1:経路に含める,0:経路に含めない marker/skipの組み合わせでこんな感じになります。 marker skip 説明 1 0 (既定値)Marker生成して経路に含まれる 0 0 Markerなしの通過点となり、細かい経路設定が可能 1 1 Markerのみで経路に含まれない 0 1 この設定かつlabel="-b-"で分割に使用しています。 marker=0,skip=1の場合は最初使用しない組み合わせだったんですが、あとで経路を分割する必要が生じた際に使うことにしました。 例: index id name order label marker skip 1 K1 KAMAKURA_STATION 0 Start 1 0 2 K1 HACHIMAN_GU 1 1 1 0 3 K1 HASE_DERA 2 2 1 0 4 K1 GREAT_BUDDHA 3 3 1 0 正直、placeとrouteはテーブル分けなくても良かったなって後で思いました。使うときはJoinして使うし、メンテナンス性があんまり良くなくて。 courseテーブルもいらないっちゃいらなかったんですが・・・ functions.php WordPressに組み込むため、functions.phpを記述します。 最初はスクリプト登録用の手続き <?php add_action( 'wp_enqueue_scripts', 'theme_enqueue_styles' ); function theme_enqueue_styles() { wp_enqueue_style( 'parent-style', get_template_directory_uri() . '/style.css' ); wp_enqueue_style( 'child-style', get_stylesheet_directory_uri() . '/style.css', array('parent-style') ); } function register_script(){ // 登録の項目 wp_register_script( 'maps', get_stylesheet_directory_uri() . '/js/maps.js', false, '', true); wp_register_script('googlemaps', '//maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&language=en&callback=initMap', array('maps'), '', true ); } function add_script() { // 装備の項目 register_script(); wp_enqueue_script('maps'); wp_enqueue_script('googlemaps'); } add_action('wp_enqueue_scripts', 'add_script'); 次にマップ用の関数を作ります テーブルから情報とってきてformを生成しています。 YOUR_API_KEYにはGoogle Maps Api使用する際に必要なキーを取得して埋め込んでください。 // 地図用 function gmap($atts) { //ショートコードパラメータからコース名取得 extract(shortcode_atts(array( 'course' => 'undefined', 'height' => '480px', ), $atts)); if($course=='undefined'){ echo "[gmap course='???']course not defined"; return; } // wpdbオブジェクト global $wpdb; // ★デバッグ用 $wpdb->show_errors(); // コース取得用SQL $sql_course=$wpdb->prepare(" SELECT c.id, c.travelmode, c.maptypeid, c.zoom FROM wp2_course c WHERE c.id='%s' ",$course); // ルート取得用SQL $sql_route=$wpdb->prepare(" SELECT r.id, r.name, r.order, r.label, r.marker, r.skip, p.title, p.description, X( p.latlng ) AS x, Y( p.latlng ) AS y FROM wp2_route r JOIN wp2_place p ON r.name = p.name WHERE r.id = '%s' ORDER BY r.order ",$course); // クエリ実行 $rows_course = $wpdb->get_results($sql_course); if($rows_course){ $rows_route = $wpdb->get_results($sql_route); if($rows_route){ $num = $wpdb->num_rows; echo <<< EOM <form id="{$wpdb->id}"> <input type="hidden" id="count" value="{$num}" /> EOM; foreach($rows_course as $row_course){ if($row_course->travelmode!=null){ echo "<input type='hidden' id='travelmode' value='{$row_course->travelmode}' />"; } if($row_course->maptypeid!=null){ echo "<input type='hidden' id='maptypeid' value='{$row_course->mapTypeId}' />"; } if($row_course->zoom!=null){ echo "<input type='hidden' id='zoom' value='{$row_course->zoom}' />"; } } $index=0; foreach($rows_route as $row ){ echo <<< EOM <input type="hidden" id="p{$index}-label" value="{$row->label}" /> <input type="hidden" id="p{$index}-title" value="{$row->title}" /> <input type="hidden" id="p{$index}-latlng" value="{$row->x}, {$row->y}" /> <input type="hidden" id="p{$index}-description" value="{$row->description}" /> EOM; if($row->marker==0){ echo <<< EOM <input type="hidden" id="p{$index}-marker" value="{$row->marker}" /> EOM; } if($row->skip==1){ echo <<< EOM <input type="hidden" id="p{$index}-skip" value="{$row->skip}" /> EOM; } $index=$index+1; } echo "</form>"; // echo "<div id='map' style='height: 480px'></div>"; } } return "<div id='map' style='height: {$height}'></div>"; } 最後にショートコードを追加します。 add_shortcode('gmap', 'gmap'); 使い方 WordPressダッシュボードのPagesでPageを作成し、本文に [gmap course="K1"] というふうに記述します。 ここで"K1"はコースidで設定したものを指定します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel 認証済みユーザーの取得方法

公式ドキュメント Laravel 8.x 認証 環境 PHP: 8.0.2 Laravel: 8.28.1 認証済みユーザー取得 Laravelで認証済みユーザーを取得する方法がいくつかあるのでご紹介します。 Authファサード use Illuminate\Support\Facades\Auth; // 現在認証しているユーザーを取得 $user = Auth::user(); // 現在認証しているユーザーのIDを取得 $id = Auth::id(); authヘルパー // 現在認証しているユーザーを取得 $user = auth()->user(); // 現在認証しているユーザーのIDを取得 $id = auth()->id(); Requestインスタンス(メソッドインジェクション) app/Http/Controllers/FlightController.php <?php declare(strict_types=1); namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Http\Response; final class FlightController extends Controller { /** * @param Request $request * @return Response */ public function update(Request $request): Response { $user = $request->user() // ... } } Requestインスタンス(コンストラクタインジェクション) 前述まで紹介したものの中ではこのコンストラクタインジェクションのパターンが好きです。 app/Http/Controllers/FlightController.php <?php declare(strict_types=1); namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Http\Response; final class FlightController extends Controller { public function __construct(private Request $request) { } /** * @return Response */ public function update(): Response { $user = $request->user() // ... } } リポジトリー層に認証済みユーザー取得する処理を実装する 公式で紹介されている方法はいずれもLaravelフレームワークに依存してしまいます。 フレームワークへの依存を減らすためにリポジトリー層を用意して実装する方法をご紹介します。 app/Repository/AuthenticatedUser.php [新規] app/Repository/LaravelAuthenticatedUser.php [新規] app/Providers/AppServiceProvider.php [編集] app/Http/Controllers/FlightController.php [編集] app/Repository/AuthenticatedUser.php [新規] 新しく AuthenticatedUser インターフェースを定義します。 app/Repository/AuthenticatedUser.php <?php declare(strict_types=1); namespace App\Repository; use App\Models\User; interface AuthenticatedUser { public function user(): User; public function id(): int; } app/Repository/LaravelAuthenticatedUser.php [新規] AuthenticatedUser インターフェースを実装した LaravelAuthenticatedUser クラスを作成します。 app/Repository/LaravelAuthenticatedUser.php <?php declare(strict_types=1); namespace App\Repository; use App\Models\User; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\Request; final class LaravelAuthenticatedUser implements AuthenticatedUser { public function __construct(private Request $request) { } /** * @return User * @throws AuthorizationException */ public function user(): User { $user = $this->request->user(); if ($user === null) { throw new AuthorizationException('ここから先は通さん!!!'); } return $user; } /** * @return int * @throws AuthorizationException */ public function id(): int { $id = $this->request->user()?->id; if ($id === null) { throw new AuthorizationException('ここから先は通さん!!!'); } return $id; } } app/Providers/AppServiceProvider.php [編集] AuthenticatedUser インターフェースに対応する LaravelAuthenticatedUser 実装するクラスのバインド設定をサービスプロバイダーで行います。 app/Providers/AppServiceProvider.php <?php declare(strict_types=1); namespace App\Providers; use App\Repository\AuthenticatedUser; use App\Repository\LaravelAuthenticatedUser; use Illuminate\Support\ServiceProvider; final class AppServiceProvider extends ServiceProvider { public array $bindings = [ AuthenticatedUser::class => LaravelAuthenticatedUser::class, ]; } app/Http/Controllers/FlightController.php [編集] AuthenticatedUser インターフェースをコントローラ側で使ってみます。 app/Http/Controllers/FlightController.php <?php declare(strict_types=1); namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Http\Response; final class FlightController extends Controller { public function __construct(private AuthenticatedUser $authenticatedUser) { } /** * @return Response */ public function update(): Response { dump($this->authenticatedUser->user()); dump($this->authenticatedUser->id()); // ... } } こんな感じでLaravelフレームワーク(ファサードやヘルパー)ではなくAuthenticatedUserインターフェース(抽象)に依存したコードが書けました?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】ZipArchiveを使ってS3の画像をまとめてzip化する

はじめに 業務で結構詰まってしまったので、今後のためのメモ。 また、ローカルでは動いたが、ステージングでは動かない問題にも直面したので、 それの対応策についてもまとめる。 環境 Laravelは7を使用。パッケージ管理はComposer プロジェクト開始時は8が出た直後だったので、7使ってました。 ローカル いたって普通の windows 10 pro Edition。 ローカルの環境にAWSのIAMロールは付けられないので、 開発中だけ、S3を操作できる最低限のIAMロールを持ったユーザを作成し、 アクセスキーIDや、シークレットアクセスキーを発行して使用する。 ステージング AWS EC2を使用。OSは Amazon Linux2。 インスタンス自体にS3を操作できる最低限のIAMロールを持たせており、 アクセスキーIDや、シークレットアクセスキーを使わない運用をする(予定) やること チェックボックスで選択された複数の画像をS3から取得し、zip化してダウンロードする。 この記事ではフロント側の処理は書かず、サーバ側(Laravel)の処理について書きます。 コード S3への接続準備 S3に接続するためのコードです。 アクセスキーID・シークレットアクセスキーを使用するローカルと、 EC2のIAMロールを使用するステージングで少し異なります。 $bucket = config('filesystems.disks.s3.bucket'); $imgDir = "path/to/hogehoge"; // S3に接続するための情報 $s3Config = [ 'version' => 'latest', 'region' => config('filesystems.disks.s3.region'), ]; // ローカル環境用: IAMロールを使えないので情報を追加する if (config('app.env') === 'local') { $s3Config['credentials'] = [ 'secret' => config('filesystems.disks.s3.secret'), 'key' => config('filesystems.disks.s3.key'), ]; } $s3Client = new S3Client($s3Config); ローカル用で使っている、アクセスキーID・シークレットアクセスキーを設定して接続する方法は検索すればたくさん出てきます。 検索に困ることはあまりないでしょう。 問題はIAMロールを使う方法です。 こちらの場合は $s3Config 変数の連想配列の credentials 項目が丸々ありません。 この項目がない場合の動きは、公式ドキュメントによると下記の通りです credentials オプションを指定しない場合、SDK は環境からの認証情報のロードを以下の順序で試行します。  1.環境変数から認証情報をロードする  2.credentials.ini ファイルから認証情報をロードする  3.IAM ロールから認証情報をロードします。 3番目にIAMロールの認証が使われると書いています。 credentialsをつけてしまうと、今回のステージング環境ではS3に接続できなくて動きません。 例え .env で対応する部分を空文字にしていようがエラーになってしまいます。 Storage::cloud()->get('/path/to') みたいにStorageファサードを使う場合は、.env の対応部分を空にしておけばOKなのですが、S3Clientクラスを使う場合はcredentialsオプションごと無くす必要があるのでごっちゃにならないようにしましょう。マジで。 画像取得のコマンドをまとめる 取得する画像分の情報をDBから取得し、その数だけ取得コマンドを生成します。 $imgs = []; // DBから取得する画像の情報を取得 // zipファイルの生成場所 $fileName = 'result.zip'; $filePath = storage_path('app/' . $fileName); // 画像取得コマンド $commands = []; foreach ($imgs as $img) { $key = $imgDir . $img['name']; $commands[] = $s3Client->getCommand( 'GetObject', [ 'Bucket' => $bucket, 'Key' => $key, ] ); } // 作成したコマンドの配列を実行 $command_pool_batch = CommandPool::batch($s3Client, $commands); $command 配列にはS3にあるファイルを取得するコマンドが格納されます。 'GetObject' ファイルを取得する。という命令 'Bucket' S3に生成したバケット 'key' バケット内にある、取得したい画像のパス この部分をforeachループで変化させ、選択した画像の取得コマンドを生成 最後に CommandPool::batchで、生成したコマンドを実行させます。 command_pool_batch 変数には、取得された結果が入っているので、あとはこの中身をzipに入れてDLさせます。 zip化して、DL ZipArchiveクラスを使用します。 先ほども書きましたが、command_pool_batch 変数に結果が入っているので、これをループさせます。 // zipファイル作成 $zip = new ZipArchive(); $isOpen = $zip->open($filePath, ZipArchive::CREATE | ZipArchive::OVERWRITE); // 成功時はbool型なので、型のチェックまで行う if ($isOpen === true) { foreach ($command_pool_batch as $key => $value) { // 取得できなかったらS3Exceptionが格納される if (strpos(get_class($value), 'S3Exception') !== false) { Log::warning('fail get photo'); continue; } header("Content-Type: {$value['ContentType']}"); // ファイル名を指定(しないとS3内のフルパスが使われるらしい) $fileName = $imgs[$key]['name']; $zip->addFromString($fileName, $value['Body']); } $zip->close(); ob_end_clean(); } // レスポンスヘッダー $headers = [ 'Content-Type' => 'application/zip', 'Content-Disposition' => 'attachment' ]; return response() ->download($filePath, $fileName, $headers) ->deleteFileAfterSend(true); foreach文で $command_pool_batch as $key => $value としていますが、$key にはインデックス番号が入ってきます。具体的な取得結果は $value の方です。 addFromString を使ってどんどんzipに画像の情報を追加していきます。 全て追加し終えたら、closeしてフロント側に渡しましょう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【CakePHP4】ViewのHelperからComponentを読み込む方法

読込したいComponent SimpleComponent.php <?php namespace App\Controller\Component; use Cake\Controller\Component; class SimpleComponent extends Component { /** * getHalfPoint - 与えられた数字を半分にする * * @param float * @return float */ public function getHalfPoint(float $num):float { $return_num = $num / 2; return $review_ave; } } Helperの記述(ここ大事) AppHelper.php <?php namespace App\View\Helper; use Cake\View\Helper; use App\Controller\Component\SimpleComponent; use Cake\Controller\ComponentRegistry; class AppHelper extends Helper { /** * Point - SimpleComponentのgetHalfPointの使用 */ public function Point($point):float { $this->Simple = new SimpleComponent(new ComponentRegistry()); $return_point = $this->Simple->getHalfPoint($point); return $return_point; } } 1,読み込みたいComponentを指定すること use App\Controller\Component\SimpleComponent; 2.Componentをインスタンス化するために必要なComponentRegistryを読み込む use App\Controller\Component\SimpleComponent; 3.読み込みしたいComponentのインスタンス化 $this->Simple = new SimpleComponent(new ComponentRegistry()); Viewの記述(一応) simple.php <?php echo $this->App->Point(5); // 2.5が返ってくる ?>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

生PHPについて知識を資産化していく④

前提として 自分が勉強してきた知識を保存するために書いています。 本当に初歩的な事から書いていってます。 万が一間違っている可能性もございますので、その時はご指摘頂ければ嬉しいです。 常時更新していく予定です。 アウトプット用ですので、MAMPを使用していきます。 PHPについての記事はこちらから 生PHPについて知識を資産化していく① 生PHPについて知識を資産化していく② 生PHPについて知識を資産化していく③ 生PHPについて知識を資産化していく⑤ COOKIEとSESSION ①COOKIE(クッキー) ブラウザごとに情報が保存される ・パスワード保存はNG ・近年GDPRなどで利用が制限されつつある ②SESSION(セッション) サーバー側で情報を管理している GDPR|個人情報保護委員会 Google ChromeのCOOKIEの場所 ①Google Chromeを開き、右上のタブから「設定」をクリック ②「プライバシーとセキュリティ」 → 「COOKIEと他のサイトデータ」を開く ③「すベてのCookieとサイトデータを表示」をクリックする ④残っているCOOKIE情報が出てくる ⑤試しに「localhost」と検索すると、テストしていた内容が表示される COOKIEとSESSIONの動きが分かる簡単なコードを作成してみる sessiontest_1.php <?php session_start(); //sessionを含める時には冒頭に必ず書く ?> <!DOCTYPE html> <html> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- Bootstrap CSS --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous"> <title>Hello, world!</title> </head> <body> <?php if (!isset($_SESSION['visited'])) { //設定されていなかったら echo '最初の訪問です'; $_SESSION['visited'] = 1; //訪問時に visitedを1 を受け取る $_SESSION['date'] = date('c'); //受け取る日付 } else { //設定されていたら $visited = $_SESSION['visited']; $visited++; //visitedの値を 1つ 増やす $_SESSION['visited'] = $visited; //増やした値を visited に入れる echo $_SESSION['visited'] . '回目の訪問です<br>'; if (isset($_SESSION['date'])) {//その時に date があったら日付を出す echo '前回訪問は' . $_SESSION['date'] . 'です'; $_SESSION['date'] = date('c'); //現在の日付を 'date'に入れる } } ?> </body> </html> ①最初の訪問時 ②2回目の訪問 ③3回目の訪問 var_dumpで見てみる 4回目の訪問です 前回訪問は2021-04-04T07:28:10+00:00です array(2) { ["visited"]=> int(4) ["date"]=> string(25) "2021-04-04T07:33:05+00:00" } array(1) { ["PHPSESSID"]=> string(26) "ここにCOOKIEの値が入っています" } ブラウザでデータを確認する SESSIONを破棄するファイルを作成 sessiontest_2.php <?php session_start(); ?> <!DOCTYPE html> <html> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- Bootstrap CSS --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous"> <title>Hello, world!</title> </head> <body> <?php echo 'セッションを破棄しました'; // 空の配列を入れることで、セッション情報を空で上書きする $_SESSION = []; // クッキーにもセッションIDが残っているので削除する if (isset($_COOKIE['PHPSESSID'])) { // PHPSESSIDに空の情報を入れつつ、過去の日付を入れて削除する setcookie('PHPSESSID', '', time() - 1800, '/'); } // セッションを破棄する session_destroy(); echo 'セッション'; echo '<pre>'; var_dump($_SESSION); echo '</pre>'; echo 'クッキー'; echo '<pre>'; var_dump($_COOKIE); echo '</pre>'; ?> // ブラウザを読み込むと、作成したセッション情報が破棄される </body> </html> php.net:セッション処理 php.net:setcookie() ログイン機能の簡単実装サンプル(PHP/MySQL) いろいろな関数 引数にデフォルト値を設定可能 function/defaultValue.php <?php ini_set("display_errors", 1); error_reporting(E_ALL); // = をつけると初期値になる function defaultValue($string = null) //引数を設定しないとエラーになる { echo $string . 'です'; } // 引数なし defaultValue(); //です と表示 echo '<br>'; // 引数あり defaultValue('テスト'); //テストです と表示 ?> タイプヒンティング(型を明示できる) function/type_hinting.php <?php // declare(strict_types=1); // 強い型指定 より固く宣言しないと動かない ini_set("display_errors", 1); error_reporting(E_ALL); echo 'タイプヒンティングテスト' . '<br>'; /** * @param $string */ // 型の指定なし function noTypeHint($string) { var_dump($string); } noTypeHint(['テスト']); // 引数string予定に 配列→ エラーではない  ↓↓  //エラーではない  //タイプヒンティングテスト  //array(1) { [0]=> string(9) "テスト" } echo '<br>'; // タイプヒンティング (引数に型を指定。 型が違うとエラー) // typeTest(string(型), $string(引数)) function typeTest(string $string) // 引数stringの他に、array,callable, bool, float, int, object, クラス名, インターフェース名 { var_dump($string); } typeTest(['配列文字']); // 引数にstringと指定しているのに配列→ こちらはエラー  ↓↓ // エラーになる  // Fatal error: Uncaught TypeError: Argument 1 passed to typeTest() must be of the type string,〜 ?> php.net:型宣言 php.net:declare ※PHPは動的型付であり、関数(型, 引数)だと弱い可能性があるので、「declare」などは大人数で作成するときに使われる可能性がある。 コンパイラとインタプリタとは?現役エンジニアが分かりやすく解説 動的型付けと静的型付け 可変変数 function/valiableLengthArgs.php <?php // 可変引数(関数の引数に ドット「.」を3つ付けてから変数を書く) // 複数の変数をまとめて指定することができる function combine(string ...$name): string // 戻り値の型宣言 { $combinedName = ''; for($i = 0; $i < count($name); $i++) { $combinedName .= $name[$i]; if($i != count($name) - 1) { $combinedName .= '・'; } } return $combinedName; } $lName = '名前'; $fName = '苗字'; $name1 = combine($fName, $lName); echo '結合結果: ' . $name1 ; echo '<br>'; $variableLength = combine('テスト1', 'テスト2', 'テスト3'); echo $variableLength; ↓↓ // 結合結果: 苗字・名前 // テスト1・テスト2・テスト3 と表示され、複数の変数を処理してくれている ?> php.net:新機能・戻り値の方宣言 array_map 引数に関数(コールバック関数)を入れることができる function/arrayMap.php <?php $parameters = [' 空白あり ', ' 配列 ', ' 空白あり ']; echo '<pre>'; var_dump($parameters); echo '</pre>'; // array_map(引数に関数) 配列の値それぞれに関数を適用する $trimedParameters = array_map('trim', $parameters); //trim:空白を削除する関数 echo '<pre>'; var_dump($trimedParameters); echo '</pre>'; ↓↓ array(3) { [0]=> string(14) " 空白あり " [1]=> string(8) " 配列 " [2]=> string(14) " 空白あり " } array(3) { [0]=> string(12) "空白なし" [1]=> string(6) "配列" [2]=> string(12) "空白なし" } ?> コールバック関数 引数に関数を入れる <?php function combineSpace(string $firstName, string $lastName): string { return $lastName . ' ' . $firstName; } $nameParam = ['名前', '苗字']; // コールバック関数(引数に関数を入れる) function useCombine(array $name, callable $func) { $concatName = $func(...$name); print($func.'関数での結合結果: ' . $concatName . '<br>'); } useCombine($nameParam, 'combineSpace'); ?> php.net:コールバック PHPについての記事はこちらから 生PHPについて知識を資産化していく① 生PHPについて知識を資産化していく② 生PHPについて知識を資産化していく③ 生PHPについて知識を資産化していく⑤
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ペチオブ】仮想環境ハンズオン 第5回 Docker Compose編

Docker Composeハンズオン ハンズオン資料 Linux基礎編 VirtualBoxハンズオン Vagrantハンズオン Dockerハンズオン Docker Composeハンズオン 今回の目的 今回の目的は、VirtualBox上にVagrantを使ってUbuntu OSのインストール、PHPの実行環境の構築を行います。 YouTube版 【ペチオブ】仮想環境ハンズオン 第5回 Docker Compose編 動画を高評価、チャンネル登録いただけるとより多くの方へオススメ表示されるので良いと思った方はご協力いただけるとありがたいです? 完成版 完成版はGitHubにプッシュしてます。 概要 Composeは、複数コンテナのDockerアプリケーションを定義して実行するためのツールです。 Docker単体だとコンテナ毎にコマンドやオプションをたくさん指定する必要がありましたが、複数のコンテナに対して実行できます。 ハンズオン $ mkdir docker-compose-handson $ cd docker-compose-handson サンプルソースをgit cloneしておく $ git clone https://github.com/ucan-lab/infra-handson-shop shop php/Dockerfile $ mkdir php $ vim php/Dockerfile FROM php:7.4-apache-buster ENV DB_HOST=db ENV DB_NAME=shop ENV DB_USER=phper ENV DB_PASS=secret RUN apt-get update RUN docker-php-ext-install pdo_mysql DB_HOST=db ここだけDockerハンズオンと異なります。 コンテナ名ではなく、サービス名で接続できます。 mysql/Dockerfile Dockerハンズオンと同じDockerfileを作ります。コピペしてきてもokです。 $ mkdir mysql $ vim mysql/Dockerfile FROM mysql:8.0 ENV MYSQL_DATABASE=shop ENV MYSQL_USER=phper ENV MYSQL_PASSWORD=secret ENV MYSQL_ROOT_PASSWORD=secret COPY ./my.cnf /etc/my.cnf mysql/my.cnf Dockerハンズオンと同じmy.cnfを作ります。コピペしてきてもokです。 $ vim mysql/my.cnf [mysqld] character_set_server = utf8mb4 # 文字コード collation_server = utf8mb4_ja_0900_as_cs # 照合順序 docker-compose.yml $ vim docker-compose.yml version: "3.8" services: web: build: ./php ports: - 8200:80 volumes: - ./shop:/var/www/html db: build: ./mysql volumes: - db-storage:/var/lib/mysql volumes: db-storage: YAML (ヤムル) YAML 公式 YAML wiki yamlの書き方 YAML Ain't a Markup Language インデント(字下げ)を使って、データの階層構造を表現する。 そのため、インデントが崩れると実行できなくなるので注意する。 docker-compose.yml オプション説明 version https://docs.docker.com/compose/compose-file 現時点の最新バージョンは3.8 バージョン2系の記述は古いので注意 services コンテナを定義します。 web, db サービスを定義(サービス≒コンテナ) build Dockerfile が存在するディレクトリを指定する https://docs.docker.com/compose/compose-file/#build ports ポートを公開します。(ポート転送) ホストのポート:コンテナのポート volumes ホストパスまたは名前付きボリュームをマウントします。 environment コンテナ内で使用できる環境変数を追加します。 サービスのコンテナをビルド $ docker-compose build Building web ... Successfully built b85983a00e56 Successfully tagged docker-compose-handson_web:latest Building db ... Successfully built 4077115f617f Successfully tagged docker-compose-handson_db:latest サービスのコンテナを作成、開始 $ docker-compose up -d Starting docker-compose-handson_web_1 ... done Starting docker-compose-handson_db_1 ... done -d, --detach Detached mode: Run containers in the background, print new container names. バックグラウンドでコンテナが起動します。 コンテナの一覧 $ docker-compose ps Name Command State Ports -------------------------------------------------------------------------------------------- docker-compose-handson_db_1 docker-entrypoint.sh mysqld Up 3306/tcp, 33060/tcp docker-compose-handson_web_1 docker-php-entrypoint apac ... Up 0.0.0.0:8200->80/tcp 補足1: Name 補足: db がサービス名、docker-compose-handson_db_1 がコンテナ名です。 db サービスは 3306/tcp, 33060/tcp のポートが起動してます。 補足2: Command Commandに書かれてるコマンドが現在実行中、または最後に実行されたコマンドが表示されています。 補足3: State コンテナのステータスです。 db, webサービスが起動(State: Up)していることを確認します。 補足4: Ports db 3306: 従来のMySQLプロトコルポート 33060: Xプロトコルと呼ばれるMySQLの新しい接続方式のプロトコルポート web 0.0.0.0:8200->80: ホスト側の8200ポートへアクセスするとwebコンテナの80ポートへ MySQL 初期データ設定 dbコンテナのmysqlコマンドを実行してMySQLへログインします。 $ docker-compose exec db bash $ mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE テストデータを入れます。 create table shop.items ( id int primary key auto_increment, name varchar(30) comment '商品名', stock int default 0 comment '在庫数' ) comment='商品テーブル'; INSERT INTO shop.items (name, stock) VALUE ("Apple", 10); INSERT INTO shop.items (name, stock) VALUE ("Bad Apple", 1); 動作確認 http://127.0.0.1:8200 ショップアプリがphpを実行してmysqlからデータの取得や更新が行えていればokです。 さいごに お疲れ様でした! Docker Compose ハンズオンいかがでしたでしょうか。 コンテナをまとめて起動、停止できる docker-compose はとても便利ですね。 ボリュームやネットワークの設定も簡単に行えるようになりました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelで404, 500ステータスコードを返すシンプルなAPI実装

仕事の関係で、Laravel+Vueの開発をやっています。Laravelを使うのは初めてなので、学んだものを記録します。 学んだこと Web APIの基本作りです。 今回はステータスコードのみ、非常にシンプルなレスポンスを返すWeb APIを作成してみました。 手順としては、 - API Controllerの作成 - Routeの設定 以上です。 API Controllerの作成 app/Http/Controllersの配下にTestHttpStatus.phpを作成しました。 e404, e500という二つの関数を作成し、それぞれ400, 500のステータスコードを設定します。 use App\Http\Controllers\Controller; use Illuminate\Http\Request; class TestHttpStatus extends Controller { public function e400(Request $request) { return response()->json([], 404, ['Content-Type' => 'application/json'], JSON_UNESCAPED_UNICODE); } public function e500(Request $request) { return response()->json([], 500, ['Content-Type' => 'application/json'], JSON_UNESCAPED_UNICODE); } } Route routes/api.phpにAPIのルートを追加する。 use App\Http\Controllers\TestHttpStatus; ... Route::get('test_e404', [TestHttpStatus::class, 'e404'])->name('e404'); Route::get('test_e500', [TestHttpStatus::class, 'e500'])->name('e500'); http://localhost/test_e404にアクセスすると400エラーが発生します。 http://localhost/test_e500にアクセスすると500エラーが発生します。 応用 404, 500エラーをテストしたい時に使えると思います。 また、Routeの部分ですが、routes/api.phpではなく、routes/web.phpにルートに設定するとカスタマイズの404, 500ページも検証できると思いました。 次回のチャンスがある際にメモします。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP json_encodeとjson_decodeについて

比較 encodeはphpの記述(文字列、数値、オブジェクト、配列など)をjson形式に decodeはjsonの記述をphpの記述に それぞれ変更する 参考 encode decode
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

file_get_contentsで取得したファイルにfile_put_contentsで記述を追加する方法

テキスト追加の例 txt.txt このファイルはファイルプットコンテンツで書き換えられます どうぞ ご確認ください file_put.php $file = 'txt.txt'; // ファイルをオープンして既存のコンテンツを取得します $current = file_get_contents($file); echo $current; $current .= "\n"."この行は追加ファイルです。\n"; // 結果をファイルに書き出します file_put_contents($file, $current); 上記実行後のtxt.txt このファイルはファイルプットコンテンツで書き換えられます どうぞ ご確認ください この行は追加ファイルです。 テキスト上書きの例(file_put.php) <?php $file = 'txt.txt'; $new_txt .= "新規記述です\n"; // 結果をファイルに書き出します file_put_contents($file, $new_txt); 実行後のtxt.txt 新規記述です 以上。 応用すれば条件によってファイルの出力や取得を使って色々出来そう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP $_SEVERについて

各オプションの使い方 $_SEVER['PHP_SELF'] 上記公式の情報を参考に検証していく MAMPで上記画像の様な階層で検証する。(ドキュメントルート はhtdocs) htdocs/test2/do/dec1.php 上記ファイル内で <?php echo $_SERVER['PHP_SELF']; を実行すると test2/do/dec1.php が表示される。 次に htdocs/test2/sever.php 上記ファイル内で同様に試すと test2/sever.php と表示される。 この例から公式で http://example.com/foo/bar.php での表示が /foo/bar.php となっていることが ドキュメントルートに対してそれ以下のディレクトリとファイル名が表示されると分かった。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP substr()の引数について

引数についての考察 substr 例) var_dump(substr('abcdef', 0, 4)); // "abcd" var_dump(substr('abcdef', -3, 1)); // "d" var_dump(substr('abcdef', -3, -1)); // "de" var_dump(substr('abcdef', -3, -2)); // "d" var_dump(substr('abcdef', -3, -3)); // "" 第一引数:対象文字列 第二引数:ポインタの位置 第三引数:ポインタから正の方向にn文字 or 文末から負の方向に移動し、オフセットから負の方向に移動した分を除いた文字を表示 第三引数が難しい印象だ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP ヒアドキュメントとナウドキュメントの書き方

ヒアドキュメント ヒアドキュメント PHPドキュメント 例) $name = '太郎'; print <<<HERE これはヒアドキュメントです。 改行も効きます。変数はこう入れます→私の名前は"$name"です。 終わりのIDはインデントはしてはいけません。セミコロンを忘れずに。 HERE; //結果 /* これはヒアドキュメントです。 改行も効きます。変数はこう入れます→私の名前は"太郎"です。 終わりのIDはインデントはしてはいけません。セミコロンを忘れずに。 */ 注意点 ・ヒアドキュメントの開始と終了のID(今回でいうtestID)は英数字、アンダースコアを含んだ数字ではない文字またはアンダースコアで開始する。 つまりこの内容を守れば自由に設定可能。 ・変数は" "で括ると使用可能。 ・よくある記述は EOD(End of Document) EOM(End of Message) EOF(End of File) ・終了IDはインデントしてはいけない(以下公式ドキュメントより) ナウドキュメント ヒアドキュメントがダブルクォートで囲んだ文字列として扱われるのに対して、 Nowdoc はシングルクォートで囲んだ文字列として扱われる Nowdoc の書き方は、ヒアドキュメントと同じように <<< を使用。 しかし、その後に続く識別子をシングルクォートで囲んで <<<'EOT' のようにする ヒアドキュメントの識別子に関する決まりがすべて Nowdoc の識別子にも当てはまる 特に終了識別子の書き方に関する決まりに注意する(インデント不可の件) 例) print <<<'NOW' これはナウドキュメントです。 改行も効きます。変数は文字列になります。→私の名前は"$name"です。 終わりのIDはインデントはしてはいけません。セミコロンを忘れずに。 NOW; //結果 /* れはナウドキュメントです。 改行も効きます。変数は文字列になります。→私の名前は"$name"です。 終わりのIDはインデントはしてはいけません。セミコロンを忘れずに。 */ 以上。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CMSの仕組みが垣間見えた。

備忘録です。 PHP学習してまだ間も無いですが、CMSの仕組みが垣間見えた気がする。 HPによくあるプレスリリースなどのコンテンツを管理したい場合などに便利かなと思います。 例)news記事を読み込んで、画面表示したい。 htdocsの中に格納してあるsample.phpで、別フォルダで管理しているnews.txtを読み込んで表示したい場合は、 sample.php内でfile_get_contents()メソッドを使用。()にはnews.txtのフォルダパスを記載。 $news = file_get_contents('../../news_data/news.txt'); print($news); ※私の場合は、悪意のあるアクセスを避ける為に、news.txtのフォルダをhtdocsの外に移動させました。 注意点:開発環境によってはフォルダへのアクセス権限がなく、画面表示がうまくいかない場合がある。その際は、フォルダ権限を注意深く確認する。 これを使いこなせれば、シンプルかつ強力なCMSが構築できるのでは無いかとワクワクした次第です。 環境:MacOS、MAMP、GoogleChrome
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP5上級試験/準上級試験の上級合格に挑戦(17) プログラミングPHP第3版 9章

9章 グラフィックス ※ 黒本には載ってない部分を基本的に掲載していく phpファイルで画像も作れる(ex. image1.phpなど) これにパラメータを指定することもできる (ex. <img src="image1.php?num=1">とか、<img src="image1.php?text=ok"> とか) ※たとえば$_GET['num']とかしてパラメータによって画像を変化させる、、、なんてこともできる 逆に画像タグからok.pngを生成するPHPスクリプトを起動させることもできる (ex. <img src="buttons/ok.png"> など) 基本グラフィックス作成コード index.php //imageキャンパスを作る $image = imagecreate(200,200); //色を変数に格納 $white = imagecolorallocate($image,0xFF,0xFF,0xFF); $black = imagecolorallocate($image,0x00,0x00,0x00); //塗りつぶされた正方形を作成 imagefilledrectangle($image,50,50,150,150,$black); //具現化 header("Content-Type: image/png"); imagepng($image); 9.3.3 サポートする画像のフォーマットの確認 複数の環境で動作させる場合にはimagetypes()で、その環境が適しているタイプを調べることができる index.php //imageキャンパスを作る $image = imagecreate(200,200); //色を変数に格納 $white = imagecolorallocate($image,0xFF,0xFF,0xFF); $black = imagecolorallocate($image,0x00,0x00,0x00); //塗りつぶされた正方形を作成 imagefilledrectangle($image,50,50,150,150,$black); //調べてその設定で送信 if (imagetypes() & IMG_PNG){ header("Content-Type: image/png"); imagepng($image); } else if (imagetypes() & IMG_JPG){ header("Content-Type: image/jpg"); imagepng($image); } else if (imagetypes() & IMG_GIF){ header("Content-Type: image/gif"); imagepng($image); } 9.4.2 TrueTypeFont TrueTypeはアウトラインフォントの標準規格。 これを追加するには imagettftextを使う imagettftext(image,size,angle,x,y,color,font,text) index.php //通常はこんな感じ //imageキャンパスを作る $image = imagecreate(350,70); //色を変数に格納 $white = imagecolorallocate($image,0xFF,0xFF,0xFF); $black = imagecolorallocate($image,0x00,0x00,0x00); //GDがTrueTypeフォントを探すパスを設定 putenv("GDFONTPATH=" . realpath('.')); imagettftext($image, 20, 0, 10, 40, $black, 'courbi',"Courier TrueType")); header("Content-Type: image/png"); imagepng($image); index.php //縦書きはこんな感じ //imageキャンパスを作る $image = imagecreate(70,350); //色を変数に格納(RGB形式で記載も可能) $white = imagecolorallocate($image,255,255,255); $black = imagecolorallocate($image,0,0,0); //GDがTrueTypeフォントを探すパスを設定 putenv("GDFONTPATH=" . realpath('.')); //270は上から下に向かう縦書きになる 90度は逆 imagettftext($image, 20, 270, 28, 10, $black, 'courbi',"Courier TrueType")); header("Content-Type: image/png"); imagepng($image); 9.5 動的に作成するボタン index.php $font = "times"; $size = isset($_GET['size']) ? $_GET['size] : 12; $text = isset($_GET['text'] ? $_GET['text'] : ''; $image = imagecreatefrompng("button.png"); $black = imagecolorallocate($image,0,0,0); if ($text){ $tsize = imagettfbbox($size, 0, $font, $text); $dx = abs($tsize[2] - $tsize[0]); $dy = abs($tsize[5] - $tsize[3]); $x = (imagesx($image) - $dx) / 2; $y = (imagesy($image) - $dy) / 2 + $dy; //text描画  imagettftext($image, $size, 0, $x, $y, $black, $font, $text); } header("Content-Type: image/png"); imagepng($image); 上記で無地のボタンが作れる。 そして、パラメータを下記のようにすると、パラメータがボタンに表記される。 index.php <img src="button.php?text=PHP+Button" /> スペースが+になる でもエンコードするなら index.php <img src="button.php?text=<?= urlencode("PHP+Button"); ?>" /> となる。 9.5.1 ボタンのキャッシュ ボタンのキャッシュを簡易的に作成 index.php $font = "times"; $size = isset($_GET['size']) ? $_GET['size] : 12; $text = isset($_GET['text'] ? $_GET['text'] : ''; $path = "/tmp/buttons"; //ボタンをキャッシュするディレクトリ //キャッシュ内容を送信(新規に書いた部分) if ($bytes = @filesize("{$path}/{$text}.png")) { header("Content-Type: image/png"); header("Content-Length: {$bytes}"); exit; } //画像作成して送信、キャッシュ $image = imagecreatefrompng("button.png"); $black = imagecolorallocate($image,0,0,0); if ($text){ $tsize = imagettfbbox($size, 0, $font, $text); $dx = abs($tsize[2] - $tsize[0]); $dy = abs($tsize[5] - $tsize[3]); $x = (imagesx($image) - $dx) / 2; $y = (imagesy($image) - $dy) / 2 + $dy; //text描画  imagettftext($image, $size, 0, $x, $y, $black, $font, $text); //画像をファイルに保存(新規に書いた部分)  imagepng($image, "{$path}/{$text}.png"); } header("Content-Type: image/png"); imagepng($image); 9.5.2 より高速なキャッシュ Apacheのディレクティブを使うとPHPスクリプトを作らずして一度作った画像をキャッシュから読み込める 1) Webサーバーのドキュメントルート配下のどこかにbuttonsというディレクトリを作成 2) Apacheのhttpd.confファイルに以下を追加 (buttonsディレクトリにないファイルがリクエストされたら、button.phpを実行させる指定) index.php <Location /buttons/> ErrorDocument 404 /button.php </Location> 3) 以下をbutton.phpとして保存でOK index.php //リダイレクト前のURLのパラメータがあれば取得 parse_str($_SERVER['REDIRECT_QUERY_STRING']); $cacheDir = "/buttons/"; $url = $_SERVER['REDIRECT_URL']; //拡張子ゲット $extension = substr($url, strrpos($url,'.')); //文字列$urlからディレクトリと拡張子を取り除く $file = substr($url, strlen($cacheDir), -strlen($extension)); //ファイル名から..を取り除く $file = str_replace('..','',$file); //ボタンに表示するテキスト $text = urldecode($file); $font = "times"; $path = "/tmp/buttons"; //ボタンキャッシュディレクトリ //画像作成して送信、キャッシュ $image = imagecreatefrompng("button.png"); $black = imagecolorallocate($image,0,0,0); //テキストの位置を算出 if ($text){ $tsize = imagettfbbox($size, 0, $font, $text); $dx = abs($tsize[2] - $tsize[0]); $dy = abs($tsize[5] - $tsize[3]); $x = (imagesx($image) - $dx) / 2; $y = (imagesy($image) - $dy) / 2 + $dy; //text描画  imagettftext($image, $size, 0, $x, $y, $black, $font, $text); //画像をファイルに保存(新規に書いた部分)  imagepng($image, "$_SERVER['DOCUMENT_ROOT']}{$cacheDir}{$file}.png"); } header("Content-Type: image/png"); imagepng($image); ※ファイル名として使用可能な文字しかボタンにできない *ボタンを変えた場合はキャッシュにあるファイルを除去すれば新たに作成される *ちょっと上でやったように画像形式を変えることもサイズを変えることも可能  サイズはパラメータで指定すればOK(http://example.com/buttons/php.png?size=16 など) 9.7 色の処理 (1) 7ビットのアルファチャンネルがあるフルカラーの画像を作成する関数は? (2) 8ビットのパレット形式画像を作成する関数は? (3) imagecolorallocate()で最初に指定した色はどこに使う? (4) 透明度情報を含む色インデックスを作成する (1) imagecreatetruecolor() (2) imagecreate() (3) 背景色 (4) imagecolorallocatealpha(image,red,green,blue,alpha)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

他人のLaravelのプロジェクトの動かし方

他人のLaravelのプロジェクトを git clone やダウンロードでローカルに落として動かすことがあるのでその方法を紹介する。 外部ファイルのインストール $ composer update まず、目的のプロジェクトまで移動して、CLIで必要なライブラリを上記コマンドで取り入れる。 .envファイルの修正 マイグレーションファイル等を書き換える場合は .env のファイルを書き換える必要がある。 特にDBの設定部分はここによって違うので自身の環境に合わせなければならない。 .env DB_CONNECTION=DBの種類 DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=データベース名 DB_USERNAME=ユーザー DB_PASSWORD=パスワード 時々 .env のファイルが反映されない時があるのでその時はこちらの記事を参考にしたらうまく参照させることができました。 https://qiita.com/yukke7624/items/14f29d126e38a7d86646 マイグレーション $ php artisan migrate:fresh このコマンドで新たにマイグレーションを行う。 また、ローカルサーバーも下記コマンドで立ち上げられる。 $ php artisan serve
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ユーザが選択した値を保持するoptionタグ作成方法 - foreach / old() / Null合体演算子 -【Laravel】

まえがき ・先日、検索機能実装のお仕事を頂いたのですが、その中で結構時間がかかった部分を紹介したいと思います。 「セレクトボックス(optionタグ)で選択した値を、ページ遷移先でも選択状態にする」 「え?なにが難しいの?」 そう思う方もいらっしゃるでしょう… 「以下のようなコードで実現できますよ!!」 なんてうたってるサイト上に数あるコード、片っ端から全て試しましたが出来ませんでした。 問題となったコード3つと解決したコード1つの計4つのコードで発生した問題と解決方法を解説していきたいと思います。 目次 環境 要件定義 発生した問題点 問題となったコード(時系列順) 解決したコード 解説 参考文献 環境 PHP 7.4.15 Laravel 6.20.16 要件定義 以下の要件定義を実現する上で詰まりました configディレクトリに定数用のディレクトリ・ファイルを作成、そこにセレクトボックス用のデータ配列を定義 参考:Laravelで定数をつかうよ 検索結果を表示した時に、検索条件を保持し、選択されたセレクトボックスの文字列を保持 ページネーションで他ページに遷移した場合も検索条件、検索結果を保持 その他 未選択時は null Undefined variable エラーを回避 発生した問題点 1. ページ遷移時に選択した値が保持されず空欄になる 2. 空欄ではないが選択した値とは異なる値が表示される 3. 表示に成功したがユーザが選択した値を if 文の条件に使用しているため、値の未選択時に Undefined variable エラー 問題となったコード(時系列順) 1. ページ遷移時に選択した値が保持されず空欄になる blade.php <select name="term"> @foreach (config('const.term') as $termNumber) <option value="{{ $termNumber }}" @if(old('term') == $termNumber) selected @endif>{{ $termNumber }}</option> @endforeach </select> 2. 空欄ではないが選択した値とは異なる値が表示される blade.php <select name="term"> @foreach (config('const.term') as $index => $termNumber) <option value="{{ $termNumber }}" @if(old('term', $index) == $termNumber) selected @endif>{{ $termNumber }}</option> @endforeach </select> 3. 表示に成功したが、ユーザが選択した値を if 文の条件に使用しているために未選択時に Undefined variable エラー blade.php <select name="term" id="term" class="mr-2"> @foreach (config('const.term') as $termNumber) <option value="{{ $termNumber }}" @if(old('term', $inputTerm) == $termNumber) selected @endif>{{ $termNumber }}</option> @endforeach </select> @foreach (config('const.term') as $index)で使用している config('const.term')は下記のコードです。 config/const.php <?php return [ 'term' => [ '' => '', '1' => '1', '2' => '2', '3' => '3', '4' => '4', '5' => '5', '6' => '6', '7' => '7', '8' => '8', '9' => '9', '10' => '10', '11' => '11', '12' => '12', '13' => '13', '14' => '14', '15' => '15', '16' => '16', '17' => '17', '18' => '18', '19' => '19', '20' => '20' ], ]; @if(old('term', $inputTerm)の$inputTermはセレクトボックスで選択したtermの値を request オブジェクト内から取得して変数に代入したものです。以下の controller 内のメソッドにて定義し blade に渡しています。 controller /** * 検索結果画面表示 * * @param App\Article $article * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function search(Article $article, Request $request) { $inputTerm = $request->term; $inputCategory = $request->category; $inputWord = $request->word; 〜中略〜 $articles = $article->getBySearchParameters($inputTerm, $inputCategory, $inputWord); return view('articles.index', compact('inputTerm', 'inputCategory', 'inputWord', 'articles')); } 解決したコード blade.php <select name="term" id="term" class="mr-2"> @foreach (config('const.term') as $termNumber) <option value="{{ $termNumber }}" @if(old('term', $inputTerm ?? '') == $termNumber) selected @endif>{{ $termNumber }}</option> @endforeach </select> コードの変遷と変更箇所(2箇所)は以下 blade.php 1. ページ遷移時に選択した値が保持されず空欄になる @foreach (config('const.term') as $termNumber) 2. 空欄ではないが選択した値とは異なる値が表示される @foreach (config('const.term') as $index => $termNumber) 3. 表示に成功したがユーザが選択した値を if 文の条件に使用しているため、未選択時に Undefined variable エラー 4. 解決したコード @foreach (config('const.term') as $termNumber) 1. ページ遷移時に選択した値が保持されず空欄になる <option value="{{ $termNumber }}" @if(old('term') == $termNumber) selected @endif>{{ $termNumber }}</option> 2. 空欄ではないが選択した値とは異なる値が表示される <option value="{{ $termNumber }}" @if(old('term', $index) == $termNumber) selected @endif>{{ $termNumber }}</option> 3. 表示に成功したがユーザが選択した値を if 文の条件に使用しているため、未選択時に Undefined variable エラー <option value="{{ $termNumber }}" @if(old('term', $inputTerm) == $termNumber) selected @endif>{{ $termNumber }}</option> 4. 解決したコード <option value="{{ $termNumber }}" @if(old('term', $inputTerm ?? '') == $termNumber) selected @endif>{{ $termNumber }}</option> 解説 1. ページ遷移時に選択した値が保持されず空欄になる option 毎に 「この選択肢を選んでいれば、それをselectedにする」 という if 文条件を記述していますが、結果は空欄。 繰り返し処理で出力された値とユーザが選択した値が一致することで、その値を selected に出来ると思ったのですが、上手いこと一致しませんでした。 blade.php <select name="term"> @foreach (config('const.term') as $termNumber) <option value="{{ $termNumber }}" @if(old('term') == $termNumber) selected @endif>{{ $termNumber }}</option> @endforeach </select> 2. 空欄ではないが選択した値とは異なる値が表示される Laravel 5.8 ヘルパ old() old() old関数はセッションにフラッシュデーターとして保存されている直前の入力値を取得します。 old関数は第2引数に記述した値を、初期値として設定できます。つまり、 name 属性に設定された値が old() に存在すれば value に入力され、nullの場合は第2引数に記述した値がされます。これでいける気がするのですが、保持される値は、何を選んでも「20」でした。正直どんな処理でこの結果になるのか未だによく分かりません… blade.php <select name="term"> @foreach (config('const.term') as $index => $termNumber) <option value="{{ $termNumber }}" @if(old('term', $index) == $termNumber) selected @endif>{{ $termNumber }}</option> @endforeach </select> 3. 表示に成功したがユーザが選択した値を if 文の条件に使用しているために未選択時に Undefined variable エラー ユーザが選択した値を request オブジェクトから取得し old関数の第2引数に設定します。 これこそユーザが選択した値です。 ユーザが選択した値と繰り返し処理で順番に巡ってきた値とが == で true になればユーザが選択した値がselected されます! やったー!!できたー!!と思いましたがまだ不完全です。 「ユーザが選択した値がない場合」 つまり 「ユーザが値を選択しなかった場合」 では$inputTermは 未定義で null ですらありません。Undefined variable 、いわゆる未定義エラーが発生します。 どういった場面かというと、サイト接続時の初回画面表示時です。 初めて訪問するページでは、old関数で初期値に設定した値が未定義状態になるコードです。 blade.php <select name="term" id="term" class="mr-2"> @foreach (config('const.term') as $termNumber) <option value="{{ $termNumber }}" @if(old('term', $inputTerm) == $termNumber) selected @endif>{{ $termNumber }}</option> @endforeach </select> 4. 解決したコード 上記で発生した Undefined variabl エラーを解消するためには、初期値として設定した$inputTermが未定義の際、 代わりに null になるようにすれば良いのです。 これを実現したのが 「null合体演算子」 です。 $inputTermが falsy (null or 0 or undefined or false) な値の場合、??以降の値にしてくれます。 ここは三項(エルビス)演算子では対応できません。Undefined variable が発生します また、以下のような特徴もあります。 A ?? Bの A の存在チェックもしてくれます。issetいらなくなるのかな? A が falsy の場合のみBを参照するので、??は falsy なものだけをフィルタリングしてくれる様なイメージです。 ネストすることも可能。1つ目がなければ2つ目、2つ目がなければ3つ目のような記述 (old('term', $inputTerm ?? '$inputWord' ?? '$inputCategory' ?? '') 参考はこちら:三項演算子 Null 合体演算子 blade.php <select name="term" id="term" class="mr-2"> @foreach (config('const.term') as $termNumber) <option value="{{ $termNumber }}" @if(old('term', $inputTerm ?? '') == $termNumber) selected @endif>{{ $termNumber }}</option> @endforeach </select> これで要件定義を満たすことが出来ました! 参考文献 三項演算子 Null 合体演算子 似てるようで違う、PHPのエルビス演算子とNull合体演算子 【PHP】知らなかった比較演算子【null合体演算子】 PHPのNull合体演算子でUndefined indexを防ぐ Laravelで定数をつかうよ Laravelで定数を使うときにConfigを使う腰抜けはもう死んだ 【Laravel】 都道府県のプルダウン作る Laravelで都道府県のプルダウンメニューを作ってみる 配列を使ったプルダウンの選択肢の表示の理屈について Laravelのoldヘルパーが空の場合の初期値... bladeのフォーム欄で使うold関数 | laravel oldヘルパー関数の初期値 Laravel 5.8 ヘルパ old() Laravelのold()で配列を扱う場合のインデックスの表現方法
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel私的まとめ

Laravel

Laravel 8

概要

テストすることで学習定着すると言ってたので

目的

きわめる

routing

ルーティングとは、URLと、コントローラーなどの対応を指す。
具体的な記述は routes/*.php に記述されている。

nginx設定

きれいなURL を採用するためのnginx設定
 └example.com/a で、aControllerを見に行くための設定

location / {
    index index.php index.htm index.html;
    try_files $uri $uri/ /index.php?$query_string;
}

location ~ \.php$ {
    fastcgi_split_path_info ^(.+\/php)(/.+)$;
    fastcgi_pass php-fpm:9000;
    fastcgi_index index.php;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_path_info;
}

.env

環境設定ファイル。DB, memcache, redisなどの設定。
app自体、ログ、AWS、mail、session、pusher も // TODO

migrationファイル作成

artisanでファイルを作成する。 (Flightはサンプルクラス名)

./artisan make:migration Flight [-mfsc]  # -mfsc means migration, factory, seeder, controller

ファイルを編集して、好きなテーブルレイアウトにする

        Schema::create('テーブル', function (Blueprint $table) {
            $table->id();
            $table->string('name', 50);  // 追加
            $table->timestamps();
        });

migration実行

./artisan migrate

migration全戻し

./artisan migrate:reset

model作成

./artisan make:model ${モデル名}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP で exec 関数を使ってブラウザから呼び出した命令を実行できない時の対処法

PHP を使ってボタンを実装してブラウザから押して,何らかの処理を実行しようとしてエラーを吐かれた経験がある人はかなり多いのではないかと思います.
その中でいくつか特に詰まった点を解説していきたいと思います.
主にパスが通っていないことが問題であることが多いので,パスを確認するのがいいと思います.

1. ログの出し方

まず,デバッグをするにはエラーメッセージを出さないと始まりません.
hoge 命令を実行し,そのエラーメッセージを確認したい時は以下のようにします.

exec("hoge 2>&1",$dum,$rtn); // 2>&1 で標準エラー出力も標準出力へ吐き出す
echo('<pre>'); // フォーマットを整える
var_dump($dum); // 標準出力 (標準エラー出力を含む) をブラウザに出力
echo('<pre>');
echo("return: $rtn"); // hoge 命令の返り値を "return: 返り値" の形で出力

これで,ボタンを押した後にブラウザにエラーメッセージが表示されるはずです.
基本的にこのエラーを見て,ググれば大体は解決するはずですが,少し詰まりどころを解説していきます.

2. Not Found

エラーメッセージが例えば以下のようなものだったとします.

array(1) {
  [0]=>
  string(23) "sh: 1: allon: not found"
}
return: 127

not found の意味通り, hoge 命令が見つからないと言っています.
まず,terminal から hoge が実行されるか確認します.

$ hoge

2.1. これで実行できない場合

hoge 命令がどこにあるか調べてパスを通してください.
見つからない場合は最悪,以下のように全ファイルを調べれば見つかると思います.

$ sudo find / -type f -name "hoge"

ほとんどの場合 /path/to/bin/hoge のような形だと思うので,以下のようにすればパスが通ると思います.

$ export PATH=$PATH:/path/to/bin

毎回これをするのは面倒なので,.bashrc.zshrc などに追記しておきましょう.

$ echo 'export PATH="$PATH:/path/to/bin"' >> ~/.bashrc

これで問題が解決することはないと思うので,次の 2.2. 実行できた場合 に進みましょう.

2.2. 実行できた場合

ユーザの環境では hoge が見つかっているが,PHP は見つけられないということになります.
つまり,PHP が使っている環境ではパスが通っていないということになります.

では,PHP のパスを確認してみましょう.

echo(getenv("PATH"));

このようにして,再度ブラウザでボタンを押すと同じではないかもしれませんが,以下のようにパスが表示されているはずです.

/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

この中には /path/to/bin はないはずなので,これを足してあげれば動くはずです.

echo(getenv("PATH"));
echo('<br>');
$PATH = getenv("PATH");
putenv("PATH=$PATH:/path/to/bin");
echo(getenv("PATH"));

このようにしてみると以下のように表示されてパスが変更されたのがわかると思います.

/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/path/to/bin

再度試してみるとエラー出力がなくなって,0 が返ってきている (正常終了している) ことがわかると思います.

array(0) {
}
return: 0

後は,余計な出力を消せば完成です.

3. ModuleNotFoundError

Python の関数を実行した時に起きる可能性のあるエラーです.
/path/to/hoge.py を実行することを考えてみます.

exec("python /path/to/hoge.py 2>&1",$dum,$rtn);

普通にローカル環境では動くのに,以下のようなエラーが出ることがあります.

array(4) {
  [0]=>
  string(34) "Traceback (most recent call last):"
  [1]=>
  string(70) "  File "/path/to/hoge.py", line 13, in "
  [2]=>
  string(34) "    import selenium"
  [3]=>
  string(47) "ModuleNotFoundError: No module named 'selenium'"
}
return: 1

つまり,selenium というモジュールは見つからないと言われているわけです.

3.1 実行されている Python を確認

まずは実行されている Python を確認しましょう.

exec("which python",$dum,$rtn);

ログの出力は以下のようになると思いますが,ローカルで which python を実効した結果と一致しているか確認してみてください.

array(1) {
  [0]=>
  string(16) "/usr/bin/python"
}
return: 0

一致していなければ(というかそもそも),実行する時に以下のようにフルパスで書いておくとよいです.

exec("/usr/bin/python /path/to/hoge.py 2>&1",$dum,$rtn);

3.2. Python が同じでも動かない

これで,同じ Python を実行しても動かない場合に集中できます.
ローカルではモジュールはあるのに,PHP で動かす時にはモジュールがないということなので,PHP からはアクセスできない場所にある,つまりパスが通っていないということになるはずです.

3.2.A. グローバルにモジュールをインストールする方法

簡単に解決する方法は,このモジュールをグローバルにインストールすることです.

$ sudo pip install selenium

これで,グローバルな環境にモジュールがインストールされたはずなので,PHP からモジュールが読み込めるはずです.

3.2.B. モジュールのパスを通す方法

一方,モジュールをグローバルにインストールするのは躊躇われるという場合もあるかと思います.
その場合はパスを通しましょう(この方法は筆者の場合は失敗しました…).
まずはモジュールの場所を調べないといけません.
ここで,ユーザのホームディレクトリを /path/to/home/ としておきます.

$ python
Python 3.7.3 (default, Jan 22 2021, 20:04:44)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import selenium
>>> print(selenium.__path__)
['/path/to/home/.local/lib/python3.7/site-packages/selenium']
>>> import sys
>>> print(sys.path)
['', '/usr/lib/python37.zip', '/usr/lib/python3.7', '/usr/lib/python3.7/lib-dynload', '/path/to/home/.local/lib/python3.7/site-packages', '/usr/local/lib/python3.7/dist-packages', '/usr/lib/python3/dist-packages']
>>> exit()

このようになっていたので,筆者の場合は /path/to/home/.local/lib/python3.7/site-packages にパスが通っていればいいということになり,実際 sys.path の結果に含まれていることがわかります.

一方,PHP でのパスを確認するために,path_test.py を作成して,実行します.

path_test.py
import sys
print(sys.path)
import selenium
exec("/usr/bin/python /path/to/home/path_test.py -y -t 2>&1",$dum,$rtn);

実行結果は以下のようになりました.

array(1) {
  [0]=>
  string(169) "['/path/to/home', '/usr/lib/python37.zip', '/usr/lib/python3.7', '/usr/lib/python3.7/lib-dynload', '/usr/local/lib/python3.7/dist-packages', '/usr/lib/python3/dist-packages']"
  [1]=>
  string(34) "Traceback (most recent call last):"
  [2]=>
  string(51) "  File "/home/pi/path_test.py", line 3, in "
  [3]=>
  string(19) "    import selenium"
  [4]=>
  string(47) "ModuleNotFoundError: No module named 'selenium'"
}
return: 0

確かに,/path/to/home/.local/lib/python3.7/site-packages は含まれておらず,selenium も import できていません.
そこで,path_test.py を以下のように変更してみます.

path_test.py
import sys
# パスを追加
sys.path.append("/path/to/home/.local/lib/python3.7/site-packages")
print(sys.path)
import selenium

これでいける!!!!と思ったら無理でした…

array(5) {
  [0]=>
  string(216) "['/path/to/home', '/usr/lib/python37.zip', '/usr/lib/python3.7', '/usr/lib/python3.7/lib-dynload', '/usr/local/lib/python3.7/dist-packages', '/usr/lib/python3/dist-packages', '/path/to/home/.local/lib/python3.7/site-packages']"
  [1]=>
  string(34) "Traceback (most recent call last):"
  [2]=>
  string(52) "  File "/home/pi/path_test.py", line 10, in "
  [3]=>
  string(19) "    import selenium"
  [4]=>
  string(47) "ModuleNotFoundError: No module named 'selenium'"
}
return: 1

しっかりとパスも追加されていますが,import には失敗しました.
原因を探っていると以下のことがわかりました.

$ cd /path/to/home
$ ls -la
drwx------  5 user user  4096 Jul  2  2020 .local

つまり,.local ディレクトリにアクセス権がありませんでした.
無理やり以下のようにアクセス権を与えてもいいのですが,セキュリティ上あまりよろしくなさそうなので,諦めました.

$ chmod -R 744 .local # これは少し危険です

3.2.C. モジュールをコピーしてくる方法

先程惜しいところまで行ったので諦めきれずに,path_test.py ファイルがあるディレクトリ /path/to/home にはパスが通っていることを利用して,そのディレクトリに selenium のディレクトリを持ってきました (強引).

$ cp -r /path/to/home/.local/lib/python3.7/site-packages/selenium /path/to/home
$ chmod -R 744 selenium

これで,めでたくアクセスできるようになりました!

path_test.py
import sys
print(sys.path)
import selenium
array(1) {
  [0]=>
  string(216) "['/path/to/home', '/usr/lib/python37.zip', '/usr/lib/python3.7', '/usr/lib/python3.7/lib-dynload', '/usr/local/lib/python3.7/dist-packages', '/usr/lib/python3/dist-packages']"
}
return: 0

権限のところで詰まってしまいましたね…
まだまだ修行が必要です.

4. まとめ

PHP の exec 関数を使って実行する際に起こりうるエラーの内詰まったものをいくつか解説しました.
環境などをいじるのは難しいですが,少しずつ慣れていきたいところですね.
PHP で sudo 命令を実行するというのもやってみたので,時間があればまた記事を書きたいと思います.
ぜひお楽しみに :thumbsup:

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