20201215のPHPに関する記事は30件です。

Laravel de パスワードチェッカー

昨今では辞書型攻撃が......
という前置きはゴミ箱に捨てておいて、漏洩が確認されたパスワードを公開しているHave I Been PwnedのAPIを活用し、脆弱なパスワードで登録できないようなバリデーションを作成していきます。

APIのドキュメントを読む

今回は、送信されたパスワードが漏洩履歴があるのか、またどれだけあるのかどうかを知りたいので、Pwned PasswordsのAPIを活用していきます。

詳細は、ドキュメントを読んでいただきたいのですが、要約すると、

  1. パスワードをSHA1でハッシュ化。
  2. 先頭5文字を送信(大文字小文字問わず)
  3. 先頭5文字までで一致したものすべてを、6文字目以降と漏洩回数ともに返却
  4. なんやかんやしてちょ

です。

実装しようぜ

土台作り

cmd
> php artisan make:rule SafePassword
app\Rules\SafePassword.php
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class SafePassword implements Rule
{
    /**
     * Create a new rule instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        //
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'The validation error message.';
    }
}

これで土台部分はできました。

ゴリゴリ書く

app\Rules\SafePassword.phpのpasses()の中
// パスワードをSHA1でハッシュ化
$hash_password  = strtoupper(hash('sha1', $value));

// ハッシュ値の先頭5文字を変数定義
$first_section  = substr($hash_password, 0, 5);

// ハッシュ値の6文字目以降を変数定義
$second_section = substr($hash_password, 5);


// cURLリソースの新規作成
$curl_handle = curl_init('https://api.pwnedpasswords.com/range/'.$first_section);

// cURLの設定
curl_setopt_array($curl_handle, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 3
]);

// 結果を変数に保存
$response = curl_exec($curl_handle)."\n";

// そもそも結果の中に存在するか検索
if (preg_match("/$second_section:\d*\r?\n/", $response) === 1) {

    // 存在した場合、「何回」漏洩したか取得
    $count = (int) preg_replace("/([\s\S]*)$second_section:(\d*)\r?\n([\s\S]*)/", "$2", $response);

    // 漏洩回数の許容値をconfigで取得(直接定義しても可)
    $allow_leak_time = (int) config('auth.allow_password_leak_time');

    // もし、許容以上の漏洩回数ならfalse
    if ($count > $allow_leak_time)
        return false;

    // 許容以下ならtrue
    return true;
}

// そもそも漏洩してなかったらtrue
return true;

実際に指定する

適当なController,Request
'password' => ['required', 'confirmed', 'min:8', \App\Rules\SafePassword()]

コード書いてみて

最初のほうはforeachを使ったりして正規表現を使わないように努力しましたが結局正規表現が楽でした。。
似たような正規表現を二回使っているのはあまり美しくない(と個人的には)思っているのでおいおい修正していきたいと思います。
(強引すぎるという自覚はあるので「こんな方法いいよ!」等ありましたらお教えいただけると幸いです。)

作成後記

23:59作成終了!

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

全角スペースと半角スペースが複数あった場合に1つの半角スペースに戻すやり方【PHP】

業務中になるほどと思ったこと

/**
    public function multiTrim($s)
    {
        return trim(preg_replace("/ {2,}/", " ", preg_replace("/ /", " ", $s)));
    }

解説

内側のpreg_replaceでは「引数データを対象の文字列」として全角スペースから半角スペースに置換を行う

preg_replace("/ /", " ", $s)

外側のpreg_replaceでは上記の「全角スペースから半角スペースに置換した文字列」を対象として、半角スペースが2回以上繰り返される場合に1つの半角スペースとして置換を行う。

preg_replace("/ {2,}/", " ", preg_replace("/ /", " ", $s))
※{2,}は直前の表現を2回以上繰り返すという意味で、カンマがないと2回と繰り返すという意味になる

一番外側のtrim()では、
外側のpreg_replacenによって「半角スペースが2回以上繰り返される場合に1つの半角スペースとして置換された文字列」を対象に空白を取り除く作業を行う。

trim(preg_replace("/ {2,}/", " ", preg_replace("/ /", " ", $s)))
※空白を取り除ける対象はstring型であるということ
※trim()は文字列の最初と最後から空白文字を取り除く
※trim()の第二引数がないので半角空白が対象の一部となっている。(もしここで全角空白のままtrim()を行うと全角空白はtrim()の対象外なのでtrim出来ない。)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EC-CUBEのバージョンアップを見据えたカスタマイズ方法2020年度版

この記事はGit を使って EC-CUBE を簡単アップデートの2020年度版です。

EC-CUBE はバージョンアップが大変なことで有名?でしたが、 4系になって EC-CUBEアップデートプラグイン が用意されるなど、徐々に改善されてきています。

しかし、本体のコードをカスタマイズしてしまうと、バージョンアップ時に上書きすることもできず、アップデートプラグインでもエラーが表示され、バージョンアップの難易度が格段に上ってしまいます。
「プラグインや Customize ディレクトリでカスタマイズしろよ」 ってことでしょうが、何でもかんでもプラグインや Customize ディレクトリで頑張ろうとすると、開発・保守コストも上がってしまいます。
高度なプログラミングスキルも求められるため、なかなか簡単にはいきません。

EC-CUBE4系の場合、カスタマイズ方法は大きく分けて3種類あります。

  1. Customize ディレクトリでカスタマイズ
  2. プラグインでカスタマイズ
  3. 本体のファイルを上書き

この中で、1 の Customize ディレクトリは EC-CUBE4系から用意された方法です。
2系や3系では 2 or 3 の2択となります。

ここで、本当にプラグインにする必要があるのか? Customize ディレクトリを使う必要があるのか? 今一度考えてみましょう。

  • 別の案件などで再利用したい。オーナーズストアで販売したい
  • アンインストール機能が必要
  • Git でソースコードのバージョン管理ができない
  • ec-cube.co を使っている

クラウド版である ec-cube.co は、カスタマイズ可能な箇所が限られているため、必然的にプラグインでカスタマイズすることになります。

バージョンアップ楽になるからプラグインで... と、よく言われるのですが それは大きな間違いです。

プラグインを使ってカスタマイズしても、本体がバージョンアップした時に、処理が干渉すれば影響を受けます。
干渉しているかどうかは、テストで動かすまでわかりません。
干渉しているかどうか、差分を取るのも苦労します。

プラグインや Customize ディレクトリは実装が複雑化しやすく、脆弱性を生みやすい懸念もあります。
実装上の依存関係がわかりづらいため、引き継ぎをした場合に後任者が大変な思いをすることも。。。

また、すべてのカスタマイズを Customize ディレクトリやプラグインでできるわけではありません。
規模が大きくなると必然的に本体のカスタマイズも必要になってきます。

Git でしっかりバージョン管理しつつ、本体ファイルを上書きするのがカスタマイズの開発効率も、バージョンアップも楽で開発効率のバランスが取れています。(当社比)

開発工数自体は本体をカスタマイズした方が圧倒的に少ないです。
また、バージョンアップも Git を使用した方が安全・確実です。

影響のあるところはコンフリクトするので、バージョンアップ時のテストも楽です。 git diff で差分の一発でわかります。
hub コマンドを使えば、脆弱性パッチもコマンド一つで当てられます。

# Pull Request #4575 を取り込む
hub merge https://github.com/EC-CUBE/ec-cube/pull/4575

3系、4系でデザイン管理画面からテンプレートを編集すると app/template 以下にファイルがコピーされます。アップデートプラグインを使用していても、手動で差分を確認する必要が出てきます。このため、テンプレートを Git で管理することが難しくなりますので注意しましょう

それでも本体はさわりたくないという人は否定しませんが、 Git でバージョンアップする手順を見ていきましょう。

この方法は 2系、3系でも利用可能です。
ただし、すべての状況において万能なアップデート方法ではありませんので、自己責任でお願い致します。

また、汎用的にするため、すべてコマンドラインで記載していますが、 SourceTree などの GUI ツールを使っても大丈夫です。

1. github から EC-CUBE のソースコードを clone します

  • <eccube version> には、使用したい EC-CUBE のバージョン(例: 4.0.4)
  • <folder name> には、宛先のフォルダ名を入力してください
EC-CUBE4.x
git clone https://github.com/EC-CUBE/ec-cube.git -b <eccube version> <folder name>
EC-CUBE3.x
git clone https://github.com/EC-CUBE/ec-cube3.git -b <eccube version> <folder name>
EC-CUBE2.x
git clone https://github.com/EC-CUBE/eccube2.git -b <eccube version> <folder name>
# 2系の場合はバージョンの前に eccube- を付与してください
# 例) eccube-2.4.4

2. 管理用のブランチを作成

<new-branch-name> には、ソースコードを管理するためのブランチ名(例: production) を入力します

cd <folder name>
git checkout -b <new-branch-name>

これで、カスタマイズしたソースコードを管理するための準備が整いました。
本番環境などからソースコードを取得し、 <folder name> に上書きしてください。

3. 差分の確認

カスタマイズしたソースコードと、 EC-CUBE 本体のソースコードに差分が発生しているかを git status コマンドで確認します。

git status

On branch production
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   src/Eccube/Repository/OrderRepository.php

no changes added to commit (use "git add" and/or "git commit -a")

src/Eccube/Repository/OrderRepository.php に修正が入っていることがわかります。

どのような内容の修正か、 git diff コマンドで見てみましょう。

git diff

diff --git a/src/Eccube/Repository/OrderRepository.php b/src/Eccube/Repository/OrderRepository.php
index f80ee50e8d..d41217427c 100644
--- a/src/Eccube/Repository/OrderRepository.php
+++ b/src/Eccube/Repository/OrderRepository.php
@@ -113,7 +113,7 @@ class OrderRepository extends AbstractRepository
             $qb
                 ->andWhere('o.id = :multi OR o.name01 LIKE :likemulti OR o.name02 LIKE :likemulti OR '.
                             'o.kana01 LIKE :likemulti OR o.kana02 LIKE :likemulti OR o.company_name LIKE :likemulti OR '.
-                            'o.order_no LIKE :likemulti OR o.email LIKE :likemulti OR o.phone_number LIKE :likemulti')
+                            'o.order_no LIKE :likemulti OR o.email LIKE :likemulti OR o.phone_number LIKE :likemulti OR o.message LIKE :likemulti')
                 ->setParameter('multi', $multi)
                 ->setParameter('likemulti', '%'.$searchData['multi'].'%');
         }

受注管理画面の検索条件に、お問い合わせ内容が追加されています。

4. 修正内容の保存

この修正内容を、 git addgit commit コマンドで保存しておきましょう。

git add .
git commit -m 'save production'

[production 37fb0e7] save production
 1 file changed, 1 insertion(+), 1 deletion(-)

git add . では、最後に「.(ドット)」を入れるのを忘れないようにしてください。
git commit-m には、任意のコミットログを記録できます。どんな修正なのか書いておくと、後々役立ちます。

こうして日々の修正をコミットしておくと、差分の確認ができたり、万が一修正ミスが発生した時でも、安心して元に戻すことができます。

5. バージョンアップ

EC-CUBE の新バージョンがリリースされましたので、バージョンアップしましょう。

git fetch origin <eccube version> コマンドで、最新のソースを取得できます。
ここでは 4.0.5 のソースを取得します。バージョン番号は適宜読み替えてください。

git fetch origin 4.0.5

From https://github.com/EC-CUBE/ec-cube
 * tag               4.0.5      -> FETCH_HEAD

この時、 git pull は使用しないのがコツです。 git pull を使用すると、予期せぬ更新が反映されてしまう場合があります。

取得した差分を適用(merge)しましょう。

git merge 4.0.5

Updating 8710496..2d2b958
Fast-forward
 .gitignore                                         |    2 +-
 .gitmodules                                        |    2 +-
 .travis.yml                                        |    9 +-
 README.md                                          |   19 +-
 app/console                                        |    1 -
 appveyor.yml                                       |   11 +-
 composer.json                                      |    7 +-
 composer.lock                                      |  912 +++++++++++-------
 docs
...snip

大量にログが出力されますが、落ち着きましょう。
もう一度、 git status で正常に適用されたか確認しましょう。
改修した本体のソースコードが上書きされていないことも確認しましょう。

git status

On branch production
nothing to commit, working directory clean

nothing to commit, working directory clean と出ていれば、大丈夫です。

composer.jsoncomposer.lock が変更されていた場合は、 以下のコマンドで vender を更新しましょう。

curl -sS https://getcomposer.org/installer | php
php ./composer.phar selfupdate --1
php ./composer.phar install --dev --no-interaction

この後、ソースコードをサーバーへアップロードすればアップグレード完了です。

データベースに変更が入っていた場合は、以下のコマンドでマイグレーションを実行しましょう

## データベースの更新を確認
bin/console doctrine:schema:update --dump-sql

## 更新がある場合はマイグレーションを実行
bin/console doctrine:schema:update --dump-sql --force

番外) 競合(コンフリクト)してしまった時は?

git merge の時に、ソースコードの修正が被ってしまい、バージョンアップの適用に失敗する場合があります。

git merge 4.0.5

Auto-merging src/Eccube/Repository/OrderRepository.php
CONFLICT (content): Merge conflict in src/Eccube/Repository/OrderRepository.php
Removing src/Eccube/Form/Type/ShoppingType.php
Removing src/Eccube/DependencyInjection/Compiler/TemplateListenerPass.php
Removing repos/.gitkeep
Removing html/user_data/.gitkeep
Removing html/template/default/assets/css/maps/style.css.map
Removing html/template/admin/assets/css/maps/bootstrap.css.map
Removing html/template/admin/assets/css/maps/app.css.map
Removing .github/workflows/action.yml
Removing .github/actions/setup-chromedriver/tsconfig.json
Removing .github/actions/setup-chromedriver/src/setup-codeception.ts
Removing .github/actions/setup-chromedriver/package.json
Removing .github/actions/setup-chromedriver/package-lock.json
Removing .github/actions/setup-chromedriver/lib/setup-codeception.sh
Removing .github/actions/setup-chromedriver/lib/setup-codeception.js
Removing .github/actions/setup-chromedriver/jest.config.js
Removing .github/actions/setup-chromedriver/docs/contributors.md
Removing .github/actions/setup-chromedriver/action.yml
Removing .github/actions/setup-chromedriver/__tests__/run.test.ts
Removing .github/actions/setup-chromedriver/README.md
Automatic merge failed; fix conflicts and then commit the result.

最後に Automatic merge failed; fix conflicts and then commit the result. という行が出たら修正が競合しています。

上記の例では、 src/Eccube/Repository/OrderRepository.php が CONFILICT となっています。

git status コマンドでも確認できます。

git status

On branch production
You have unmerged paths.
  (fix conflicts and run "git commit")

Changes to be committed:

    new file:   .devcontainer/devcontainer.json
    modified:   .dockerignore
    new file:   .editorconfig
    modified:   .env.dist
    modified:   .env.install
    new file:   .github/.htaccess

...snip

Unmerged paths:
  (use "git add <file>..." to mark resolution)

    both modified:   src/Eccube/Repository/OrderRepository.php

最後に Unmerged paths: と出ているのが、競合してしまったファイル一覧です。

src/Eccube/Repository/OrderRepository.php を開くと、競合した箇所を確認できます。
<<<<<<< で検索するとすぐに見つかります。

            $qb
<<<<<<< HEAD
                ->andWhere('o.id = :multi OR o.name01 LIKE :likemulti OR o.name02 LIKE :likemulti OR '.
                            'o.kana01 LIKE :likemulti OR o.kana02 LIKE :likemulti OR o.company_name LIKE :likemulti OR '.
                            'o.order_no LIKE :likemulti OR o.email LIKE :likemulti OR o.phone_number LIKE :likemulti OR o.message LIKE :likemulti')
=======
                ->andWhere('o.id = :multi OR CONCAT(o.name01, o.name02) LIKE :likemulti OR '.
                            'CONCAT(o.kana01, o.kana02) LIKE :likemulti OR o.company_name LIKE :company_name OR '.
                            'o.order_no LIKE :likemulti OR o.email LIKE :likemulti OR o.phone_number LIKE :likemulti')
>>>>>>> 4.0.5
                ->setParameter('multi', $multi)
                ->setParameter('likemulti', '%'.$clean_key_multi.'%')

======= より上が、アップグレード前のソースコード、下がアップグレード後のソースコードです。
この場合は、上の方を残したいので、下の方を削除して正しいソースコードに修正します。

                ->andWhere('o.id = :multi OR o.name01 LIKE :likemulti OR o.name02 LIKE :likemulti OR '.
                            'o.kana01 LIKE :likemulti OR o.kana02 LIKE :likemulti OR o.company_name LIKE :likemulti OR '.
                            'o.order_no LIKE :likemulti OR o.email LIKE :likemulti OR o.phone_number LIKE :likemulti OR o.message LIKE :likemulti')

修正ができたら、コミットしておきましょう。

git add .
git commit -m 'save production'
[production b6da7e2] save production

小々長くなってしまいましたが、こうすることで、本体のソースコードに手を入れても、比較的簡単にアップデートすることができます。
プラグインを使っていても、本体の修正と競合する問題を完全に防ぐことは難しいです。
Git をうまく活用して、開発効率と安定性を両立させましょう。

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

[Laravel]早めのクリスマスviewが真っ白問題

Laravelのviewが真っ白だ。

題名の通り、なぜかLaravelのviewが真っ白になったので、そのことについて残しておきます。ちなみに原因はめちゃめちゃ単純なことでした。

環境

  • windows10
  • Laravel6
  • php(7.4.6)

やろうとしていたこと

TestController.php
class TestController extends Controller
{
  public function index($id)
  {
   $users = Users::find($id);

   return view('test',['users' => $users]);
  }

  public function post()
  {
   return view('test.view');
  }
web.php
Route::get('/test/{id}', 'testController@index');

Route::get('/test/post', 'testController@post');

てきな記述をしていたのですが、真っ白状態です。。。。
ググっていったところ、web.phpの順番が問題そう。

web.php
Route::get('/test/post', 'testController@post');

Route::get('/test/{id}', 'testController@index');

変えてみたら、直った!

原因

結果原因はweb.phpの順番というよりは、{id}の部分が邪魔していたそう。
最初の通り、

web.php
Route::get('/test/{id}', 'testController@index');

Route::get('/test/post', 'testController@post');

このように記載していると、最初のRouteで/test/{id}を読み込んでしまい、test/postと送っても、ルーティングでpostをidと判断してしまい、そのようなidはないため、真っ白になっていた。
そのため、順番を変えると、先にtest/postが先に読み込まれるので、viewを返したのですね。
このままでも動くのですが、何かの間違いでまたidを読み込まれてもめんどくさいので、このように直しました!

web.php
Route::get('/test/{id}', 'TravelController@showDetail')->where('id', '[0-9]+');

こうすると、idは数字しかとってこなくなり、先ほどの順番を変えても、先に読み込まれることはなくなりました。

まとめ

以上がLaravelからのクリスマスプレゼントでした、この先も色々なプレゼント(エラー)をいただくと思いますが、ありがたく受け取ってうまく使いこなしていこうと思います。

参考

ルーティングを書く順番をミスって画面真っ白から抜け出せなくなった話

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

WordpressのWP-Membersプラグインで会員サイトを作ってみる

プラグインWP-members

参考:会員サイトが簡単に作成できるWP-Memberを実装してみた!
http://creatornote.nakweb.com/%E4%BC%9A%E5%93%A1%E3%82%B5%E3%82%A4%E3%83%88%E3%81%8C%E7%B0%A1%E5%8D%98%E3%81%AB%E4%BD%9C%E6%88%90%E3%81%A7%E3%81%8D%E3%82%8Bwp-member%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%97%E3%81%A6%E3%81%BF%E3%81%9F/

https://webfun-style.com/wp-members/

ショートコード

hoge.txt
[wpmem_form login]
//ログインフォームを出力。

[wpmem_form register]
//新規登録フォームを出力。

[wpmem_profile]
//ユーザープロフィールを出力。

[wpmem_form user_edit]
//ユーザープロフィールの編集フォームを出力。

[wpmem_logout]
//ログアウトページの出力

[wpmem_form password]
//パスワードの変更・リセット

[wpmem_form forgot_username]
//ユーザー名を忘れた場合の回復するためのEメール入力フォーム。

[wpmem_logged_in]ログインユーザーのみ[/wpmem_logged_in]
//ショートコードで囲んだテキストがログインユーザーのみに表示

[wpmem_logged_out]ログアウトユーザーのみ[/wpmem_logged_out]
//ショートコードで囲んだテキストがログアウトユーザーのみに表示

[wpmem_field user_login]
//ユーザー名を表示

[wpmem_avatar]
//プロフィール画像を表示

[wpmem_field user_email]
//Eメールアドレスを表示

[wpmem_login_link]ログイン[/wpmem_login_link]
//ログインページへのリンクとショートコードで囲んだリンクテキスト

[wpmem_reg_link]新規登録[/wpmem_reg_link]
//新規登録ページへのリンクとショートコードで囲んだリンクテキスト。

ユーザー関連の処理

hoge.php
<?php 

  //ユーザー情報を取得
  $user = wp_get_current_user();
  echo $user->ID; //ユーザーID
  echo $user->user_login; //ログインID
  echo $user->user_nicename; //サニタイズ後のログインID
  echo $user->user_email; //登録メールアドレス
  echo $user->user_status; //ユーザーステータス
  echo $user->display_name; //WordPress上の表示名

  //ログインされていたら
  if(is_user_logged_in()){
    //ここに処理を記述
  }

  // 権限グループ【管理者】であれば
  if (current_user_can('administrator')) {
    //ここに処理を記述
  }

  // 権限グループ【購読者】であれば
  if (current_user_can('subscriber')) {
    //ここに処理を記述
  }


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

【CakePHP】bin/cake bake migration_diff 差分を生成できない。

前提・実現したいこと

bin/cake bake migration_diff

でDBとの差分を生成したい。

発生したエラーメッセージ

Your migrations history is not in sync with your migrations files. Make sure all your migrations have been migrated before baking a diff.

Google翻訳

移行履歴が移行ファイルと同期していません。 差分をベイクする前に、すべての移行が移行されていることを確認してください。

原因

・cakeのapp内にある、マイグレーションファイルと、DB内にあるphinxlogテーブルが同じ状態でなかったためエラーを吐いていた。

対応

app内の方を基準としたかったので、phinxlogテーブルにある不要レコードを削除し、実行したら通りました。

開発環境

MAMP
cakephp3.7.3

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

Method Illuminate\Http\UploadedFile::getClientSize does not exist. と出た場合の対処法

結論

top.php
$request->file('file')->getClientSize()
                //変更する
$request->file('file')->getSize()

新しいバージョンのLaravelだとgetClientSize()は使えないみたい(;´Д`)

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

PHP授業 データベースに接続するコード②(SNS編)

SNSを作る

MAMP使ってやります。コードの部分を上から綺麗に整理していくから、余分なところもあるけど全部合わせれば動くよ

まずはフォームの作成

ユーザ名→uname
タイトル→title
本文→message
削除パスワード→dpw

<form method="POST" action="./php07.php">
  ユーザ名: <input type="text" name="uname"><br />
  タイトル: <input type="text" name="title"><br />
  本文: <textarea name="message" cols="40" rows="10"></textarea><br />
  削除パスワード: <input type="password" name="dpw"><br />
<input type="submit">
</form>

エラー出た時の処理

<?php
#エラー表示の設定
#error_reporting(E_ALL);
#error_reporting(E_ALL & ~ E_DEPRECATED & ~ E_USER_DEPRECATED & ~ E_NOTICE);
error_reporting(E_COMPILE_ERROR & ~ E_RECOVERABLE_ERROR & ~ E_ERROR & ~ E_CORE_ERROR);

データベースに接続

使うデータベースはgms!!
MAMPなのでパスワード必要です。

$dsn = 'mysql:host=localhost;dbname=gms;charset=utf8mb4';
$username = 'root';
$password = 'root';

値の格納

フォーム入力した値をデータベースに運びます。

$pdo = new PDO($dsn,$username,$password)
or die('Could not connect. ');
if(isset($_POST['uname']) && strlen($_POST['uname'])>0){
  $uname=$_POST['uname'];
}
if(isset($_POST['title']) && strlen($_POST['title'])>0){
  $title=$_POST['title'];
}
if(isset($_POST['message']) && strlen($_POST['message'])>0){
  $message=$_POST['message'];
}
if(isset($_POST['dpw']) && strlen($_POST['dpw'])>0){
  $dpw=$_POST['dpw'];
}
if(isset($_POST['delpid']) && strlen($_POST['delpid'])>0){
  $delpid=$_POST['delpid'];
}

投稿の処理、削除の処理

投稿の時は、insertを使って新しいレコードを追加してる。
phpsnsテーブルのそれぞれのカラムに、valueで値を入れて…

削除の時は、定義してたdpwと入力されたdpwが一緒なら、deleteでレコードが消される。
この時、phpsnsテーブルの指定されたpidカラムが消されるようになっている?

#メッセージが入力されていた場合は投稿
if (isset($message)){
$sql="insert into phpsns(uname,title, message,pdate,dpw)
  values('" . $uname . "','" . $title . "','" . $message .
  "',current_date,'" . $dpw . "');";
  $stmt = $pdo->prepare($sql);
  $stmt -> execute();
}
#削除のIDが入力されていた場合は削除
if (isset($delpid) && isset($dpw)){
  $sql="delete from phpsns where pid='" . $delpid . "' and dpw='" . $dpw .
"';"; //この時、phpsnsテーブルの指定されたpidカラムが消されるようになっている?
  $stmt = $pdo->prepare($sql);
  $stmt -> execute();
}

表示する(ここでいうタイムライン)

一応、投稿ごとにラインも入るようになっている

#投稿されている全てのメッセージを表示
$sql2="select pdate,title,uname,message,pid,dpw from phpsns order by pid desc;";
$stmt = $pdo->prepare($sql2);
$stmt -> execute();
#$result = $stmt->fetch(PDO::FETCH_ASSOC);
$result = $stmt->fetchall();
foreach ($result as $line) {
  echo $line[title] . " (" . $line[pdate] . ") by " . $line[uname] . "<br>" .$line[message];
  echo "<form method=\"POST\" action=\"./php07.php\">" .
  "<input type=\"hidden\" name=\"delpid\" value=\"" . $line[4] .
  "\">" .
  "削除パスワード: <input type=\"password\" name=\"dpw\">" .
  "<input type=\"submit\" value=\"削除\">" .
  "</form><hr>";
}
?>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP授業 データベース接続のためのコード②(SNS編)

SNSを作る

MAMP使ってやります。コードの部分を上から綺麗に整理していくから、余分なところもあるけど全部合わせれば動くよ

まずはフォームの作成

ユーザ名→uname
タイトル→title
本文→message
削除パスワード→dpw

<form method="POST" action="./php07.php">
  ユーザ名: <input type="text" name="uname"><br />
  タイトル: <input type="text" name="title"><br />
  本文: <textarea name="message" cols="40" rows="10"></textarea><br />
  削除パスワード: <input type="password" name="dpw"><br />
<input type="submit">
</form>

エラー出た時の処理

<?php
#エラー表示の設定
#error_reporting(E_ALL);
#error_reporting(E_ALL & ~ E_DEPRECATED & ~ E_USER_DEPRECATED & ~ E_NOTICE);
error_reporting(E_COMPILE_ERROR & ~ E_RECOVERABLE_ERROR & ~ E_ERROR & ~ E_CORE_ERROR);

データベースに接続

使うデータベースはgms!!
MAMPなのでパスワード必要です。

$dsn = 'mysql:host=localhost;dbname=gms;charset=utf8mb4';
$username = 'root';
$password = 'root';

値の格納

フォーム入力した値をデータベースに運びます。

$pdo = new PDO($dsn,$username,$password)
or die('Could not connect. ');
if(isset($_POST['uname']) && strlen($_POST['uname'])>0){
  $uname=$_POST['uname'];
}
if(isset($_POST['title']) && strlen($_POST['title'])>0){
  $title=$_POST['title'];
}
if(isset($_POST['message']) && strlen($_POST['message'])>0){
  $message=$_POST['message'];
}
if(isset($_POST['dpw']) && strlen($_POST['dpw'])>0){
  $dpw=$_POST['dpw'];
}
if(isset($_POST['delpid']) && strlen($_POST['delpid'])>0){
  $delpid=$_POST['delpid'];
}

投稿の処理、削除の処理

投稿の時は、insertを使って新しいレコードを追加してる。
phpsnsテーブルのそれぞれのカラムに、valueで値を入れて…

削除の時は、定義してたdpwと入力されたdpwが一緒なら、deleteでレコードが消される。
この時、phpsnsテーブルの指定されたpidカラムが消されるようになっている?

#メッセージが入力されていた場合は投稿
if (isset($message)){
$sql="insert into phpsns(uname,title, message,pdate,dpw)
  values('" . $uname . "','" . $title . "','" . $message .
  "',current_date,'" . $dpw . "');";
  $stmt = $pdo->prepare($sql);
  $stmt -> execute();
}
#削除のIDが入力されていた場合は削除
if (isset($delpid) && isset($dpw)){
  $sql="delete from phpsns where pid='" . $delpid . "' and dpw='" . $dpw .
"';"; //この時、phpsnsテーブルの指定されたpidカラムが消されるようになっている?
  $stmt = $pdo->prepare($sql);
  $stmt -> execute();
}

表示する(ここでいうタイムライン)

一応、投稿ごとにラインも入るようになっている

#投稿されている全てのメッセージを表示
$sql2="select pdate,title,uname,message,pid,dpw from phpsns order by pid desc;";
$stmt = $pdo->prepare($sql2);
$stmt -> execute();
#$result = $stmt->fetch(PDO::FETCH_ASSOC);
$result = $stmt->fetchall();
foreach ($result as $line) {
  echo $line[title] . " (" . $line[pdate] . ") by " . $line[uname] . "<br>" .$line[message];
  echo "<form method=\"POST\" action=\"./php07.php\">" .
  "<input type=\"hidden\" name=\"delpid\" value=\"" . $line[4] .
  "\">" .
  "削除パスワード: <input type=\"password\" name=\"dpw\">" .
  "<input type=\"submit\" value=\"削除\">" .
  "</form><hr>";
}
?>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP授業 データベース接続のためのコード

フォームに入力した情報をデータベースから検索できるものを作る

XAMPPやMAMPを使えば、データベースに自動的につないでくれる??よくわからないのでてチェック

まずはフォームから

今回はcountrynameを"cname"という名前で格納していく

<form action="./php06.php" method="POST">
Country Name:<input type="text" name="cname" size="20">
<input type="submit" value="send!">
</form>

MySQLを使ってデータベースにつなぐ記述

一般ユーザーはいちいちselectとか書いて検索しない…ユーザーが入力した情報をこちらで決まった場所に取り込んで検索させるプログラムを書けば良い。

<?php
if (isset($_POST['cname'])){ //もしcnameがセットされたら
  $cname=$_POST['cname']; //postでcnameを受け取る。変数cnameと定義する
  $query='select * from country where cname=\'' . $cname . '\';';
//countryテーブルの全てから、cnameに関連するものを検索。cname=さっき受け取ったcnameですよ、って言ってる
}
else{
  $query='select * from country;';  //何もcnameに入力されていなかったら、全ての情報を出す
}

DB接続の時の注意

XAMMPとMAMPでここの記述は変わるので注意

↓XAMMPの場合

$dsn = 'mysql:host=localhost;dbname=gms;charset=utf8mb4';
$username = 'root';
$password = '';

↓MAMPの場合

$dsn = 'mysql:host=localhost;dbname=gms;charset=utf8mb4'; //dbnameにはデータベース名を入れる
$username = 'root';
$password = 'root';

ここからのはちょっとよくわからん。

$pdo = new PDO($dsn,$username,$password)or die('Could not connect. ');
$stmt = $pdo->prepare($query);
$stmt -> execute();
$result = $stmt->fetchall();
$count = $stmt->columnCount();

ここから、表示するものの記述!今回はデータベースに入っている情報の表みたいなのを表示してるよ

echo "<table border=1>\n";
foreach ($result as $line) {
  echo "<tr>\n";
  for ($i = 0; $i < $count; $i++) {
    echo "<td>$line[$i]</td>\n";
  }
  echo "</tr>\n";
}
echo "</table>\n";
?>

スクリーンショット 2020-12-15 16.25.38.png

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

[PHP]複数配列のループ

複数配列を同時にループさせる方法として以下のようにarray_map()を使うのが便利だったので共有します。

.php
$array1 = [1,2,3];
$array2 = [4,5,6];

foreach (array_map(null, $array1, $array2) as [$arr1, $arr2]) {
    echo ($arr1 + $arr2); //5,7,9
}

array_map(null, $array1, $array2)の結果は [ [1,4], [2,5], [3,6] ] となっていて、
nullを渡すことによって、それぞれの配列の同じインデックスの要素同士をくっつけて、配列の配列にしてくれます。

会社の紹介

私は現在、株式会社ダイアログという物流×ITの会社に勤務しております。
2020年12月現在、エンジニアの募集はしていませんが、他にも様々な職種を募集しているので、Wantedlyのページをご覧ください。

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

Visual Studio Code PHP Debug 急にブレークポイントが設定できなくなった

目的

  • デバッグ中にブレークポイントが設定できなくなって困ったので自分の遭遇したケースの解決策を記載する

前提情報

  • 筆者のVScodeは日本語化拡張機能が導入されている。

症状

  • 行数の左側をクリックするとブレークポイントを設定できるはずなのにクリックしてもブレークポイントが設定できない

    FirstController_php_—_j-project.png

解決方法

  1. タスクバーの「code」をクリックし「基本設定」→「設定」をクリックする。
  2. 設定の検索に「phg debug」と入力しEnterを押下する。

    設定_—_j-project.png

  3. サイドメニューの「デバッグ」をクリックする。

    設定_—_j-project.png

  4. 「Debug: Allow Breakpoints Everywhere」のチェックが外れていたためチェックをつける

    設定_—_j-project.png

  5. 下記のようになっていることを確認して設定を閉じる。

    設定_—_j-project.png

  6. 正常にブレークポイントを設定できようになった。使用中になぜ設定が変わってしまったんだろう。。。

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

WordPressで投稿内容の任意の項目を必須化する

ワードプレスのテーマtwentytwentyを基盤に、投稿内容の任意の項目を必須にする実装をしたので、備忘録的にこの記事を残す。

環境情報

PHP:version 7.3.12
WordPress:version 5.5.3
WPテーマ:twentytwenty

作業

プラグインを使えば簡単にできることかもしれないが、練習がてらどうにかプラグイン無しで実装できないものかと思い見つけたやり方です。

※ポストタイプ名が blog の場合

functions.php
// 投稿時の必須項目の設定
function post_edit_required() {
?>
    <script type="text/javascript">
        jQuery(function($) {
            if( 'blog' == $('#post_type').val() ) {
                $('#post').submit(function(e) {
                // タイトル
                if ( '' == $('#title').val() ) {
                    alert('タイトルを入力してください');
                    $('.spinner').css('visibility', 'hidden');
                    $('#publish').removeClass('button-primary-disabled');
                    $('#title').focus();
                    return false;
                }
                // コンテンツ(エディタ)
                if ( $('.wp-editor-area').val().length < 1 ) {
                    alert('コンテンツを入力してください');
                    $('.spinner').css('visibility', 'hidden');
                    $('#publish').removeClass('button-primary-disabled');
                    return false;
                }
                // 抜粋
                if ( '' == $('#excerpt').val() ) {
                    alert('抜粋を入力してください');
                    $('.spinner').css('visibility', 'hidden');
                    $('#publish').removeClass('button-primary-disabled');
                    $('#excerpt').focus();
                    return false;
                }
                // カテゴリー
                if ( $('#taxonomy-category input:checked').length < 1 ) {
                    alert('カテゴリーを選択してください');
                    $('.spinner').css('visibility', 'hidden');
                    $('#publish').removeClass('button-primary-disabled');
                    $('#taxonomy-category a[href="#category-all"]').focus();
                    return false;
                }
                // タグ
                if ( $('#tagsdiv-post_tag .tagchecklist span').length < 1 ) {
                    alert('タグを選択してください');
                    $('.spinner').css('visibility', 'hidden');
                    $('#publish').removeClass('button-primary-disabled');
                    $('#new-tag-post_tag').focus();
                    return false;
                }
                // アイキャッチ
                if ( $('#set-post-thumbnail img').length < 1 ) {
                    alert('アイキャッチ画像を設定してください');
                    $('.spinner').css('visibility', 'hidden');
                    $('#publish').removeClass('button-primary-disabled');
                    $('#set-post-thumbnail').focus();
                    return false;
                }
                });
            }
        });
    </script>
<?php
}
add_action( 'admin_head-post-new.php', 'post_edit_required' );
add_action( 'admin_head-post.php', 'post_edit_required' );

※使用される際は、必須化したくない項目の箇所をコメントアウトしてご利用ください。

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

WordPressで投稿編集画面のカテゴリー欄をカスタマイズする

ワードプレスのテーマtwentytwentyを基盤に、投稿編集画面のカテゴリー欄の表示をカスタマイズする実装をしたので、備忘録的にこの記事を残す。

環境情報

PHP:version 7.3.12
WordPress:version 5.5.3
WPテーマ:twentytwenty

「よく使うもの」と「新規カテゴリーを追加」を非表示

「よく使うもの」タブと「新規カテゴリーを追加」リンクを同時に非表示にする方法をまず紹介します。

functions.php
// カテゴリーの「よく使うもの」と「新規カテゴリーを追加」を非表示
function my_admin_style() {
    echo '<style>
    div.categorydiv li.hide-if-no-js{
        display:none;
    }
    div.wp-hidden-children a.hide-if-no-js{
        display:none;
    }
    </style>'.PHP_EOL;
}
add_action('admin_print_styles', 'my_admin_style');

div.categorydiv li.hide-if-no-js で囲まれた部分が「よく使うもの」、div.wp-hidden-children a.hide-if-no-js で囲まれた部分が「新規カテゴリーを追加」をそれぞれ非表示にする内容です。使われる際は実装したい内容に合わせて適宜変更してください。

カテゴリーの選択を一つのみにする

カテゴリーで選択できるものを一つだけにする方法を紹介します。

functions.php
function category_one_select() {
?>
    <script type="text/javascript">
        jQuery(function($) {
            // 投稿画面のカテゴリー選択を制限
            var categorydiv = $( '#categorydiv input[type=checkbox]' );
            categorydiv.click( function() {
                $(this).parents( '#categorydiv' ).find( 'input[type=checkbox]' ).attr('checked', false);
                $(this).attr( 'checked', true );
            });
            // クイック編集のカテゴリー選択を制限
            var inline_edit_col_center = $( '.inline-edit-col-center input[type=checkbox]' );
            inline_edit_col_center.click( function() {
                $(this).parents( '.inline-edit-col-center' ).find( 'input[type=checkbox]' ).attr( 'checked', false );
                $(this).attr( 'checked', true );
            });
        });
    </script>
<?php
}
add_action( 'admin_print_footer_scripts', 'category_one_select' );

今回は投稿編集画面とクイック編集画面の両方でこの機能が実装されるように処理をまとめています。使われる際は実装したい内容に合わせて適宜変更してください。

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

WordPressのウィジェットを有効化する方法

WordPressウィジェットは必ず使うと思うので

テーマを開発する時は有効にしておくと良いです。

WordPressウィジェットを有効化する方法

function.php
/* ---------------------------------------
ウィジェットの有効化
--------------------------------------- */
function my_theme_widgets() {
    register_sidebar(array(
         'name' => 'サイド',
         'id' => 'side_widget' ,
         'before_widget' => '<div class="side-widget">',
         'after_widget' => '</div>',
         'before_title' => '<h2 class="widget-side-title">',
         'after_title' => '</h2>'
    ));
    register_sidebar(array(
         'name' => 'フッター',
         'id' => 'footer_widget' ,
         'before_widget' => '<div class="footer-widget">',
         'after_widget' => '</div>',
         'before_title' => '<h2 class="widget-footer-title">',
         'after_title' => '</h2>'
    ));
    }
    add_action( 'widgets_init', 'my_theme_widgets' );

サイド用とフッター用を設置してみました。

あとは使いたいページのphpファイルに下記のようにかく!!

php
//サイド
<?php dynamic_sidebar('side_widget'); ?>
php
//フッター
<?php dynamic_sidebar('footer_widget'); ?>

さいご

以上!!
ではまた!!!

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

Laravelで簡易的にBasic認証

LaravelでBasic認証をやる方法はいくつかありますが、ライブラリを入れたりせず、データベース使ったりせず、簡易的にに実現する方法です。

  • ミドルウェアを作成する(1ファイル)
  • ミドルウェアをKernelに記述する
  • routes/web.php で認証の対象のページを記述する

の3ステップで実現できます。

ミドルウェアを作成する

今回のサンプルでは、BasicAuthMiddlewareという名前でミドルウェアを作成します。

php artisan make:middleware BasicAuthMiddleware

作成したミドルウェアのhandleメソッドを、以下のように実装します。
ユーザー名とパスワードは、ここに直接書いてます。

App\Http\Middleware\BasicAuthMiddleware.php
    public function handle(Request $request, Closure $next)
    {
        $username = $request->getUser();
        $password = $request->getPassword();

        if ($username == 'sample' && $password = 'sample') {
            return $next($request);
        }

        abort(401, "Enter username and password.", [
            header('WWW-Authenticate: Basic realm="Sample Private Page"'),
            header('Content-Type: text/plain; charset=utf-8')
        ]);
    }

ミドルウェアをKernelに登録

app/Http/Kernel.php の $routeMiddleware に、作成したミドルウェアを記述。
今回は、'basicauth'という名前で登録

app/Http/Kernel.php
'basicauth' => \App\Http\Middleware\BasicAuthMiddleware::class,

バージョン5くらいのLaravelだと、下記の書き方になります。

'basicauth' => 'App\Http\Middleware\BasicAuthMiddleware',

routes/web.php で認証の対象のページを記述する

登録した名前のミドルウェアで、対象のページをグループ化する

routes/web.php
Route::group(['middleware' => 'basicauth'], function() {
    // ここに対象のページを記述
    // 例)
    Route::get('/', [App\Http\Controllers\TopController::class, 'index'])->name('top');
});

以上で完了です。

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

LINE WORKSのJWT認証をPHPで通してみる

ちょっと仕事でLINE WORKSの認証を通す必要があったのですが、
composerでjwtライブラリを入れて認証するものが多かったのでピュアPHPで入れるとこうなるって
ものを書いてみた

<?php
/**
 * @property strign rsa_key プライベートキー(実際はセキュアな場所におかないとだめ)
 */
$rsa_key = "-----BEGIN PRIVATE KEY-----
[ここは発行されたプライベートキーを入れる]
-----END PRIVATE KEY-----";

/**
 * @property strign server_id サーバーID
 */
$server_id = "[ここも発行されているサーバーIDを入れる]";

//Header作成
$header = '{"alg":"RS256","typ":"JWT"}';
$header = base64_encode($header);
//URLSafeな形で置換
$header = str_replace(array('+', '/', '='), array('-', '_', ''), $header);

//body作成 issはサーバーID
$body = '{"iss":"%s","iat":%s,"exp":%s}';
//生成は現在時刻、満了は30分後に設定
$body = sprintf($body, $server_id, time(), time() + 1800);
$body = base64_encode($body);
$body = str_replace(array('+', '/', '='), array('-', '_', ''), $body);

//headerとbodyをつなげたもの
$sha_hash = $header . "." . $body;

//電子認証を行うアルゴリズムはSHA256にしないといけない
openssl_sign($sha_hash, $encrypted, $rsa_key, OPENSSL_ALGO_SHA256);
$signature = base64_encode($encrypted);
$signature = str_replace(array('+', '/', '='), array('-', '_', ''), $signature);

//JWT生成
echo $header . "." . $body . "." . $signature;

このJWTを使ってPOSTmanで認証通してみると
Postman_と_Qiita.png

こんな感じで200OKが帰ってきて成功する
本来はライブラリでやるんでしょうけど、それが諸々の理由でできない人はこんな感じで認証できるよという話でした

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

PHP授業(授業メモ)

入力フォーム、送信ボタンを作る

フォームに入力されたものは、"namae"という名前になる。
POSTは送る!って意味かな

<form action="./php02.php" method="POST">
your name:<input type="text" name="namae" size="20">
<input type="submit" value="send!">
</form>

↓上のコードでこれができるよ
スクリーンショット 2020-12-15 10.49.10.png

入力された値を呼び出す。データの受け渡し①

namaeを表示させる。

<?php echo htmlspecialchars($_POST['namae']); ?>

htmlspecialcharsってのは、文字化けしないように、セキュリティーのためのものらしい。

入力された値を呼び出す。データの受け渡し②

もし、"namae"にisset(セット)されていたら、こうしてください。

<?php
if (isset($_POST['namae'])){
echo 'Hello, ' . htmlspecialchars($_POST['namae']);
}
?>

結果
スクリーンショット 2020-12-15 11.01.12.png

入力された値を呼び出す。データの受け渡し③

※ここからはフォームも変わるよ
txt→さっきのnamaeの部分。textを示す
no→numberを示す

<form action="./php04.php" method="POST">
text:<input type="text" name="txt" size="20"><br>
number: <input type="text" size="3" value="10" name="no">
<input type="submit" value="send!">
</form>
<?php
if (isset($_POST['txt']) && isset($_POST['no']) && (int)$_POST['no']>0){
  for($i=0;$i<(int)$_POST['no'];$i++){
    echo htmlspecialchars($_POST['txt'] . ' ');
  }
}
?>

訳:
txtとnoが入力されていて、かつnoの数字が0より大きい時、noに入力された数字のぶん、textを出力させる。ってやつ。

入力された値を呼び出す。データの受け渡し④

<form action="./php05.php" method="POST">
URL:<input type="text" name="url" size="20">
<input type="submit" value="memo">
</form>
<?php
if (isset($_POST['url'])){
$url=$_POST['url'] . "\n";
$fp = fopen("php05.dat", "a");
fwrite($fp, $url);
fclose($fp);
}
if (file_exists('./php05.dat')){
$fp = fopen("php05.dat", "r");
while ($line = fgets($fp)) {
echo "$line<br />";
}
fclose($fp);
}
else {
echo 'File not found.';
}
?>

まずはこの部分!

if (isset($_POST['url'])){
$url=$_POST['url'] . "\n";
$fp = fopen("php05.dat", "a");
fwrite($fp, $url);
fclose($fp);
}

訳:
urlという名前の値をもったら、出力。→変数urlとする。
fopenは「file open」の意味。php05.datというファイルを開く→変数fpとする
feriteは書く。つまり、ファイルに書いてけってこと!最後はファイルを閉じる。

次!

if (file_exists('./php05.dat')){
$fp = fopen("php05.dat", "r");
while ($line = fgets($fp)) {
echo "$line<br />";
}
fclose($fp);
}
else {
echo 'File not found.';
}
?>

訳:もしphp05.datがあって
書いてあったら、追加して書いてください。何も書いてなければfile not foundで。
ファイルにどんどんデータが追加されていく。php05.datは自動で作成されて自動でデータが更新されていくイメージ。

↓結果
スクリーンショット 2020-12-15 11.50.16.png

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

PHP授業(php01〜php05まで)

入力フォーム、送信ボタンを作る

フォームに入力されたものは、"namae"という名前になる。
POSTは送る!って意味かな

<form action="./php02.php" method="POST">
your name:<input type="text" name="namae" size="20">
<input type="submit" value="send!">
</form>

↓上のコードでこれができるよ
スクリーンショット 2020-12-15 10.49.10.png

入力された値を呼び出す。データの受け渡し①

namaeを表示させる。

<?php echo htmlspecialchars($_POST['namae']); ?>

htmlspecialcharsってのは、文字化けしないように、セキュリティーのためのものらしい。

入力された値を呼び出す。データの受け渡し②

もし、"namae"にisset(セット)されていたら、こうしてください。

<?php
if (isset($_POST['namae'])){
echo 'Hello, ' . htmlspecialchars($_POST['namae']);
}
?>

結果
スクリーンショット 2020-12-15 11.01.12.png

入力された値を呼び出す。データの受け渡し③

※ここからはフォームも変わるよ
txt→さっきのnamaeの部分。textを示す
no→numberを示す

<form action="./php04.php" method="POST">
text:<input type="text" name="txt" size="20"><br>
number: <input type="text" size="3" value="10" name="no">
<input type="submit" value="send!">
</form>
<?php
if (isset($_POST['txt']) && isset($_POST['no']) && (int)$_POST['no']>0){
  for($i=0;$i<(int)$_POST['no'];$i++){
    echo htmlspecialchars($_POST['txt'] . ' ');
  }
}
?>

訳:
txtとnoが入力されていて、かつnoの数字が0より大きい時、noに入力された数字のぶん、textを出力させる。ってやつ。

入力された値を呼び出す。データの受け渡し④

<form action="./php05.php" method="POST">
URL:<input type="text" name="url" size="20">
<input type="submit" value="memo">
</form>
<?php
if (isset($_POST['url'])){
$url=$_POST['url'] . "\n";
$fp = fopen("php05.dat", "a");
fwrite($fp, $url);
fclose($fp);
}
if (file_exists('./php05.dat')){
$fp = fopen("php05.dat", "r");
while ($line = fgets($fp)) {
echo "$line<br />";
}
fclose($fp);
}
else {
echo 'File not found.';
}
?>

まずはこの部分!

if (isset($_POST['url'])){
$url=$_POST['url'] . "\n";
$fp = fopen("php05.dat", "a");
fwrite($fp, $url);
fclose($fp);
}

訳:
urlという名前の値をもったら、出力。→変数urlとする。
fopenは「file open」の意味。php05.datというファイルを開く→変数fpとする
feriteは書く。つまり、ファイルに書いてけってこと!最後はファイルを閉じる。

次!

if (file_exists('./php05.dat')){
$fp = fopen("php05.dat", "r");
while ($line = fgets($fp)) {
echo "$line<br />";
}
fclose($fp);
}
else {
echo 'File not found.';
}
?>

訳:もしphp05.datがあって
書いてあったら、追加して書いてください。何も書いてなければfile not foundで。
ファイルにどんどんデータが追加されていく。php05.datは自動で作成されて自動でデータが更新されていくイメージ。

↓結果
スクリーンショット 2020-12-15 11.50.16.png

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

phpについて

この記事について

phpの学んでいくにあたって、phpについてどのようなものなのかまとめたので参考にしていただけたらありがたいです。

phpとは?

大まかに説明すると、動的にWebページを生成することのできるサーバーサイドのスクリプト言語です。動的?サーバーサイド?スクリプト言語?とわからない言葉が出てきたので各々説明していきます。

  • 動的なWebページ

アクセスした状況、タイミングに応じて内容の表示が異なるWebページのことである。
例えばユーザーが書き込み、内容が増えていく掲示板やGoggleなどの検索結果などが挙げられる。

  • サーバーサイド

プログラムを実行した際に処理される場所がサーバー側であることを言います。詳細は自身が記事で公開しているのでこちらをご覧ください。
サーバーサイドの詳細

  • スクリプト言語

可読性が高く、習得を容易にしたプログラミング言語の総称のことを言います。PHPやRuby、Pythonがこれにあたります。

主な特徴

PHPの最大の特徴は、HTMLと組み合わせて使うことができるということです。HTMLは基本的に静的サイトを作成するのもなので、記事の更新などのタイミングに応じて表示の変わる動的サイトを作ることができません。そこでHTMLで生成されたコードの一部をPHPに変更することによって動的サイトを作ることができます。

終わりに

以上が私がPHPの学習を通してPHPとは?を学んだ概要です。至らない部分があると思います。新しく学んだ際は随時追加していきますので見ていただけたら幸いです。

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

【初心者】PHPで文字列の頭文字を取得する

文字列の頭文字を取得する

文字列の頭文字を取得したい時は…

echo mb_substr("文字列", 取得開始位置, 取得したい文字数);

取得開始位置を指定する時、頭文字から取得したい時は0を指定する。(※配列の1番目が0から始まる事と似ている)
取得したい文字数は、そのまま何文字取得したいのかを指定する。

使いどころがわかってはいないが、自分のために覚えておこう。

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

PDO実装

1.はじめに

2020年12月13日に更新したphp備忘録(データベース接続)にてデータベースに接続する方法を記載した。それを踏まえ、この記事ではそのデータベースに実際にデータを追加したり、削除する方法を記載する。

2.PDO実装

2.1下準備

require_once(*****);

上記コマンドで用意したデータベース接続関数を呼び出す。

2.2実装

$sql = "INSERT INTO *** (***, ***) VALUES ('***'(:***) ,'***'(:***))";
// 関数db_connect()からPDOを取得する
$pdo = db_connect();
try {
    $stmt = $pdo->prepare($sql);
   ($stmt->bindParam(':***', $***);)
    $stmt->execute();
    echo 'インサートしました。';
} catch (PDOException $e) {
    echo 'Error: ' . $e->getMessage();
    die();
}

まず、prepareというPDOのメソッドを呼ぶ。
これは引数で渡された指示(SQL文)をMySQLに分かる形に変換する。
これを プリペアドステートメント と呼ぶ。


※SQLのVALUESには、後で何でも入れられるように一旦「:name」、「:password」のような形で仮置きする。例えば、以下のようにすると仮で置いた「:name」に$nameを入れる。といった指定をすることができる。

$stmt->bindParam(':name', $name);



話を戻して、、、
このプリペアドステートメントを作成したタイミングで値をセットしていく。
ちなみにここではセットではなく、 値をバインド(固定)すると言う。

$stmt->bindParam(':name', $name);

命令を実行するためにはexecuteを使用する。

$stmt->execute();



以上

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

WordPressでページネーションを実装する

ワードプレスのテーマtwentytwentyを基盤に、アーカイブページにページネーション機能を持たせる実装をしたので、備忘録的にこの記事を残す。

環境情報

PHP:version 7.3.12
WordPress:version 5.5.3
WPテーマ:twentytwenty

作業

ページネーションの実装には、色々な方法があると思うが、今回は折角なのでWordPressの関数を使った方法を試したいと思います。
今回編集するのはfunctions.phpとページネーションを取り入れたいページのphpファイルです。

functions.php
// ページネーション
function pagenation( $pages = '', $range = 2 ) { // $pagesは総ページ数、$rangeは前または後ろに出したい数字の数
    $showitems = ( $range * 2 ) + 1;
    global $paged; // $pagedは現在のページ番号
    if ( empty( $paged ) ) $paged = 1;
    if ( $pages == '' ) {
        global $wp_query;
        $pages = $wp_query->max_num_pages;
        if ( !$pages ) {
            $pages = 1;
        }
    }
    if ( 1 != $pages ) {
        // 画像を使う時用に、テーマのパスを取得
        $img_pass = get_template_directory_uri();
        echo "<div class=\"m-pagenation\">";
        // 「1/2」表示 現在のページ数 / 総ページ数
        echo "<div class=\"m-pagenation__result\">". $paged."/". $pages."</div>";
        // 「前へ」を表示
        if ( $paged > 1 ) echo "<div class=\"m-pagenation__prev\"><a href='".get_pagenum_link($paged - 1)."'>前へ</a></div>";
        // ページ番号を出力
        echo "<ol class=\"m-pagenation__body\">\n";
        for ( $i=1; $i <= $pages; $i++ ) {
            if ( 1 != $pages && ( ! ( $i >= $paged+$range+1 || $i <= $paged-$range-1 ) || $pages <= $showitems ) ) {
                echo ( $paged == $i ) ? "<li class=\"-current\">".$i."</li>": // 現在のページの数字はリンク無し
                    "<li><a href='".get_pagenum_link($i)."'>".$i."</a></li>";
            }
        }
        // [...] 表示
        if ( ( $paged + 4 ) < $pages) {
            echo "<li class=\"notNumbering\">...</li>";
            echo "<li><a href='".get_pagenum_link($pages)."'>".$pages."</a></li>";
        }
        echo "</ol>\n";
        // 「次へ」を表示
        if ( $paged < $pages ) echo "<div class=\"m-pagenation__next\"><a href='".get_pagenum_link( $paged + 1 )."'>次へ</a></div>";
        echo "</div>\n";
    }
}

// pre_get_posts を使ってメインクエリーを書き換える
function change_posts_query( $query ) {
    if ( is_admin() || ! $query->is_main_query() || is_preview() )
        return;
    // カスタム投稿アーカイブ
    if ( $query->is_main_query() && $query->is_post_type_archive( 'blog' ) ) {
        $query->set( 'posts_per_page', 10 ); // 10件ずつ表示
        return;
    }
}
add_action( 'pre_get_posts', 'change_posts_query' );

今回はpagesを可変に、rangeを2に、posts_per_pageを10件に設定しています。
お使いの際は適宜使用に沿った変更を加えてください。

<?php
if ( have_posts() ) :
while ( have_posts() ) : the_post();
?>
<li>
  <!-- 詳細ページへのリンク -->
  <a href="<?php the_permalink(); ?>">
  .
  . <!-- ループ内の処理を記述 -->
  .
  </a>
</li>
<?php
endwhile;
if( function_exists( 'pagenation' ) ) : // 関数が定義されていたらtrueになる
pagenation();
endif;
else : // 記事がない場合
?>
<li>
  まだ投稿がありません。
</li>
<?php
endif;
?>

表示側の記述です。liタグ内は適宜変更ください。

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

Dockerを使ってLaravelのローカル開発環境を作る(Apache版)

Laravel Advent Calendar 2020 - Qiita の 15日目 の記事です。
昨日は @noel_kuma さんのLinter / FormatterからLaravelへの贈り物の記事でした!
明日は @kyoya0819 さんの記事です!

概要

最強のLaravel開発環境をDockerを使って構築する【新編集版】

この記事の反響が多く、Apache版も作って欲しいという要望もあってので記事を書いてみました。

対象読者

  • Laravelを愛する心の持っている方
  • PHP, Linux, Git, Dockerの知識をある程度持っている方

リポジトリ

https://github.com/ucan-lab/docker-laravel-apache

使い方

A. Laravelプロジェクトの新規作成

$ git clone git@github.com:ucan-lab/docker-laravel-apache.git
$ cd docker-laravel-apache
$ make create-project

http://localhost

以上の3ステップでLaravelの新規プロジェクトの環境構築は完了です。

A'. Laravelプロジェクトをバージョン指定して新規作成

Makefile のLaravelインストール部分を書き換えます。

    docker-compose exec app composer create-project --prefer-dist "laravel/laravel=6.*" .

B. 既存のLaravelプロジェクトの環境を構築する

$ git clone git@github.com:ucan-lab/docker-laravel-apache.git

# Laravelプロジェクトを docker-laravel-apache/backend へクローンする
$ git clone git@github.com:laravel/laravel.git docker-laravel-apache/backend

$ cd docker-laravel-apache
$ make init

http://localhost

これで既存のLaravelプロジェクトの環境構築は完了です。

コンテナ構成

├── web
└── db

web, db の2つのコンテナ構成で進めます。

nginxとの違い

Nginxの場合は、php-fpm(アプリケーションサーバー)とコンテナを分けていました。
Apacheの場合は、mod_phpというモジュールがデフォルトでインストールされており、それがPHPを実行してくれるアプリケーションサーバーを兼ねるウェブサーバーです。

ディレクトリ構成

.
├── backend # Laravelプロジェクトのルートディレクトリ
├── infra
│     └── docker
│          ├── apache
│          │   └── httpd.conf
│          ├── mysql
│          │   ├── Dockerfile
│          │   └── my.cnf
│          └── php
│              ├── Dockerfile
│              └── php.ini
├── .env.example
├── Makefile
└── docker-compose.yml

nginx版との差分

※nginx版との違いはnginxのディレクトリがapacheディレクトリに変わったくらいですね?

  • infra/docker/apache/httpd.conf
  • infra/docker/php/Dockerfile
  • Makefile
  • docker-compose.yml

変更点に関しては、上記の4つのファイルに注目していただければ良いかなと思います。
プルリクエストで詳細な差分を確認できます。

PHP のベースコンテナ

イメージタグに php:<version>-apache が用意されています。

解説など

docker-compose.yml

version: "3.8"
volumes:
  db-store:
services:
  web:
    build:
      context: .
      dockerfile: ./infra/docker/php/Dockerfile
    ports:
      - ${WEB_PORT:-80}:80
    volumes:
      - ./backend:/work/backend
    environment:
      - DB_CONNECTION=mysql
      - DB_HOST=db
      - DB_PORT=3306
      - DB_DATABASE=${DB_NAME:-laravel_local}
      - DB_USERNAME=${DB_USER:-phper}
      - DB_PASSWORD=${DB_PASS:-secret}

  db:
    build:
      context: .
      dockerfile: ./infra/docker/mysql/Dockerfile
    ports:
      - ${DB_PORT:-3306}:3306
    volumes:
      - db-store:/var/lib/mysql
    environment:
      - MYSQL_DATABASE=${DB_NAME:-laravel_local}
      - MYSQL_USER=${DB_USER:-phper}
      - MYSQL_PASSWORD=${DB_PASS:-secret}
      - MYSQL_ROOT_PASSWORD=${DB_PASS:-secret}

appとwebコンテナをまとめて、webコンテナのみになりました。
どっちにするか悩みましたけど、webコンテナにしました。

解説することないので、次へ行きます。

infra/docker/php/Dockerfile

infra/docker/php/Dockerfile
FROM node:14-buster as node
FROM php:7.4-apache-buster
LABEL maintainer="ucan-lab <yes@u-can.pro>"
SHELL ["/bin/bash", "-oeux", "pipefail", "-c"]

# timezone environment
ENV TZ=UTC \
  # locale
  LANG=en_US.UTF-8 \
  LANGUAGE=en_US:en \
  LC_ALL=en_US.UTF-8 \
  # composer environment
  COMPOSER_ALLOW_SUPERUSER=1 \
  COMPOSER_HOME=/composer

# composer command
COPY --from=composer:2.0 /usr/bin/composer /usr/bin/composer
# node command
COPY --from=node /usr/local/bin /usr/local/bin
# npm command
COPY --from=node /usr/local/lib /usr/local/lib
# yarn command
COPY --from=node /opt /opt

RUN apt-get update && \
  apt-get -y install git libicu-dev libonig-dev libzip-dev unzip locales && \
  apt-get clean && \
  rm -rf /var/lib/apt/lists/* && \
  locale-gen en_US.UTF-8 && \
  localedef -f UTF-8 -i en_US en_US.UTF-8 && \
  a2enmod rewrite && \
  docker-php-ext-install intl pdo_mysql zip bcmath && \
  composer config -g process-timeout 3600 && \
  composer config -g repos.packagist composer https://packagist.org

COPY ./infra/docker/php/php.ini /usr/local/etc/php/php.ini
COPY ./infra/docker/apache/httpd.conf /etc/apache2/sites-available/000-default.conf

WORKDIR /work/backend

php, composer, nodeの3つのコンテナをマルチステージビルドして使ってます。
(phpのイメージ内にapacheも入ってます。)

nodeいらないよって場合はDockerfileから削除しちゃってください。
英語圏の方にも使ってもらえるように言語&タイムゾーンのデフォルトは英語にしてます。
実際に使うときは日本に変更してご利用ください。

a2enmod rewrite はApacheのmod_rewriteモジュールを有効化してます。
これがないとLaravelのルーティングがうまく機能しなくなります。

infra/docker/apache/httpd.conf

infra/docker/apache/httpd.conf
<VirtualHost *:80>
    ServerName example.com
    ServerAdmin webmaster@localhost
    DocumentRoot /work/backend/public

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined

    <Directory /work/backend/public>
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

最低限設定必要な項目だけ設定してます。

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

閏年じゃない年も2/29生まれの人をお祝いしたい??

これはSOUSEI Technology アドベントカレンダー2020 15日目の記事です。

2/28と3/1どちらで祝うべきなのか

2/29生まれの人、閏年なら当日お祝いしてあげればいいのですが、閏年じゃないときはどうすればいいの?
ググってみました。

例年については、男女共に「2月28に祝う」という声が圧倒的。
https://cocoloni.jp/love/232234/

あとで祝われると「忘れてたでしょ?」ってなりそう
https://teratail.com/questions/145109

ということで2/28にお祝いします!??
あとはデータを取得するだけ!

今日が誕生日の人を取得する

当日が2/28かつ閏年でないときに、追加で2/29のデータを取得してあげればOK!

$today = Carbon::now()->toDateString();
$users = User::whereDay('birthday', date('d', strtotime($today)))
    ->whereMonth('birthday', date('m', strtotime($today)))
    ->get();
if(date('m-d', strtotime($today)) == '02-28' && !date('L', strtotime($today))) {
    $users->merge(
        User::whereDay('birthday', 29)
        ->whereMonth('birthday', 2)
        ->get();
    );
}
return $users;

範囲指定(from〜to)で誕生日の人を取得する

これは指定された日付が閏年かどうか気にする必要はなく、toが2/28のときだけ考慮すればOK!

/**
 * 月日が日付範囲に含まれるスコープ
 *
 * @param Builder $query クエリ
 * @param string $column 対象カラム
 * @param Carbon $from 開始日
 * @param Carbon $to 終了日
 * @return Builder
 */
private function scopeWhereBetweenMonthAndDays($query, $column, $from, $to)
{
    // fromとtoの差が1年以上あるときはすべての日が対象になるのでクエリ追加不要(全データ取得する)
    if (!$to->diffInYears($from)) {
        $from_without_year = $from->format('m-d');
        // 閏年対応。2/28が指定された場合は2/29として、2/29のデータも取得対象とする
        $to_without_year = $to->format('m-d') == '02-28' ? '02-29' : $to->format('m-d');

        if ($from->year == $to->year) {
            $query = $query->whereRaw('DATE_FORMAT(' . $column . ',"%m-%d") >= "' . $from_without_year . '"')
                ->whereRaw('DATE_FORMAT(' . $column . ',"%m-%d") <= "' . $to_without_year . '"');
        } else {
            $query = $query->where(function ($q1) use ($from_without_year, $to_without_year) {
                $q1->orWhereRaw('DATE_FORMAT(' . $column . ',"%m-%d") >= "' . $from_without_year . '"')
                    ->orWhereRaw('DATE_FORMAT(' . $column . ',"%m-%d") <= "' . $to_without_year . '"');
            });
        }
    }
    return $query;
}

さいごに

僕の誕生日は3/3です!
プレゼント待ってます!

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

Laravel Excel から出力した csv のレコードが重複してしまっていた問題を解決した話

はじめに

あるプロジェクトで、 Laravel を使って構築したシステムの DB のデータを csv で出力する要件があり、Laravel Excel を導入して実現していました。

しかしあるとき、出力した csv のレコードが重複しているという報告があり、実際に全く同じレコードが2件重複して出力されていることがわかりました。
さらには、出力されるはずのデータが含まれていないことも判明しました。

この記事では、この問題を解決するまでの過程を紹介し、自らへの戒めと教訓としたいと思います。

Laravel Excel とは

https://laravel-excel.com

Laravel で Excel ファイルの操作を行うためのライブラリです。
PhpSpreadsheet というライブラリが元になっていて、 Laravel での操作に特化しています。
今回のプロジェクトでは、この Laravel Excel を使って、主に csv ファイルの入出力を行っています。

環境

  • PHP : 7.3.5
  • Laravel : 5.8
  • PostgreSQL : 11.3
  • Laravel Excel : 3.1.9

発生した問題

  • 約4万件のデータを抽出した際に、20〜30件ほど重複している
  • CSVを出力するたびに、重複するデータが異なる
  • データが重複した分だけ、本来であれば存在するデータがなくなっている
  • データの総件数は毎回変わらない

犯人探し

PostgreSQL

LEFT JOIN のやり方が間違っている?

とある関係で Eager Loading ではなく LEFT JOIN を使用しているので、これが真っ先に原因かと思ったのですが、単純なクエリだったのでこれは問題なさそうでした。

SELECT 文の書き方が間違っている?

SELECT でテーブル名を指定しておらず、意図しないテーブルの id などのデータが出力されてしまっているのかとも思いましたが、テーブル名を指定する書き方をしているのでこれも大丈夫そうでした。

PostgreSQL の バグ?

こんなバグがあったらもっと叩かれてそう。。

Laravel Excel

Github の issue で同じような問題を探してみる

次にライブラリである Laravel Excel のバグかと思い、 Github で同じような報告がないか探してみました。
が、これは特に見つかりませんでした。

ライブラリの中身をみてみる

中でどのような処理が行われているのかわからなかったので、中のコードを読んでみました。
今回は LaravelQueryBuilder から csv を作成していたので、途中で chunk メソッド(リファレンス) が使用されていました。
これはDBから取得するデータを小分けにすることで、メモリ使用量を抑え、大量のデータでも出力できるようにしているのだと思います。

再度考察

ここまでで具体的な原因はわからなかったので、再度原因を考察してみることにしました(実際は結構頭抱えてました)。

改めて重複しているデータを眺めてみる

よくよく見てみると、何故か重複データは2000行目や5000行目など、1000行目単位の付近で発生していました。
この 1000 という数字が何かのヒントだと思い、調べてみると、Laravel Excel の設定ファイルに chunk_size という項目があり、これが 1000 になっていました。
これを試しに 500 にしてみると、重複が1500行目や4500行目など、500の区切りでも発生するようになりました。

chunk によって発行されているクエリを調べてみる

SELECT * FROM 'テーブル名' LIMIT 1000 OFFSET 0;

そうか、わかったぞ!(cv:高山みなみ)

原因は...

ORDER BY に、ユニークになるカラムを指定していないこと でした。
今回のクエリでは、ユニークでないカラムを ORDER BY に指定していたため、このカラムが同じ値の場合、並び順がDBによって決定されてしまっていました。
これと LIMIT および OFFSET が合わさることにより、クエリを発行するたびに並び順が変更され、境目付近のデータが重複してしまっていたのだと考えられます。

この現象については、下記のブログに詳細が記載されていました。
https://blog.mmmcorp.co.jp/blog/2018/03/22/sql_order_by/

同じように LIMITOFFSET を使用しているペジネーションなんかでも注意が必要ですね。

そして ORDER BY にユニークとなるカラムを設定したところ、見事に重複がなくなりました。
これで解決ですね。

さいごに

というわけで結局犯人は自分だったわけですが。。
原因がわかるとすっきりしますが、わからないうちは辛く苦しかったです。。笑
ライブラリを使っているときに予期せぬことが起きた場合には、ライブラリの中身を見ることが大事ですね。

では最後にまとめ

並び順の権を DB に握らせるな!!

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

サムネール画像を生成する(コピペOK)

jpegファイルのサムネール画像を生成します。

①以下のコードを書きます
②mkthumbs.phpのサブディレクトリとして pics と thumbs を作成します
③picsにjpeg画像を置きます
④コマンドラインで
  % php mkthumbs.php
と入力します
⑤index.htmlをブラウザで開きます

mkthumbs.php
<?php
$dir = opendir("pics");
$pics = array();
while($fname = readdir($dir)){
  if(preg_match("/[.]jpg$/", $fname))
    $pics[] = $fname;
}
closedir($dir);

foreach($pics as $fname){
  $im = imagecreatefromjpeg("pics/$fname");
  $ox = imagesx($im);
  $oy = imagesy($im);

  $nx = 100;
  $ny = floor($oy * (100 / $ox));

  $nm = imagecreatetruecolor($nx, $ny);

  imagecopyresized($nm, $im, 0, 0, 0, 0, $nx, $ny, $ox, $oy);

  print $fname." のサムネールを生成します...\n";

  imagejpeg($nm, "thumbs/$fname");
}

print "index.html を生成しています...\n";

ob_start();
?>
<html>
<head><title>サムネール</title></head>
<body>
<table cellspacing="0" cellpadding="2" width="500">
<tr>
<?php
$index = 0;
foreach($pics as $fname){
?>
<td valign="middle" align="center">
<a href="pics/<?php echo($fname); ?>"><img src="thumbs/<?php echo($fname);
  ?>" border="0" /></a>
</td>
<?php
$index += 1;
if($index % 5 == 0) echo("</tr><tr>");
}
?>
</tr>
</table>
</body>
</html>
<?php
$html = ob_get_clean();
$fh = fopen("index.html", "w");
fwrite($fh, $html);
fclose($fh);
?>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【スクショで解説】PHP/Laravelを使ってGoogleドライブにファイルをアップロードしてみる

対象読者

  • PHP/Laravelを使ってGoogleドライブにファイルをアップロードしたい人。

コードの実装以外の部分(認証のための準備)でてこずる部分が結構あったため、メモとして保存。
サンプルコードも用意しているので、手順通りに進めれば必ず同じ結果を得られるようになっています。

下準備

コードを実装する前に、Googleドライブを外部から利用するために事前にやっておかなければならない事があるのでそちらから手をつけていきます。(APIの有効化や認証アカウントの作成など)

Google Cloud Platformの設定

https://console.cloud.google.com/
gcp_censored.jpg

まず最初にGoogle Cloud Platform(GCP)の設定を行います。

これまでGCPを利用した事の無い人はスタートガイドに従って利用を開始してください。(クレジットカードなどの情報を入力する必要がありますが、無料トライアル期間があるのでこの時点で勝手に課金される事はありません。)

参照記事: これから始めるGCP(GCE) 安全に無料枠を使い倒せ

任意のプロジェクトを選択

今回はアカウント作成時にデフォルトで用意された「My First Project」を使用します。

Google Drive APIの有効化

APIとサービス→ライブラリ.png
Google Drive API.png

左サイドバーから「APIとサービス→ライブラリ」を選択。
各種Google APIが出てくるので、その中から「Google Drive API」を見つけ、有効化します。

サービスアカウントを作成

認証情報.png

有効化に成功するとGoogle Drive APIの管理画面に飛ぶので、左サイドバーから「認証情報」を選択。
外部からGoogle Drive APIを使用する際、認証に必要な「サービスアカウント」の作成を行います。

参照記事: GCP Service Accountを理解する

サービスアカウント作成_censored.jpg
役割.png

アカウント名や説明文を入力し、「作成」ボタンをクリック。
その後、アカウントにロール(役割)を付与していきます。(今回はテストなのでProject→オーナーを選択)

③は無視でOKなので、そのまま「完了」ボタンをクリックしてください。

成功_censored.jpg

上手くいくとこんな感じでサービスアカウントが追加されています。

秘密鍵(JSON)を作成

json_censored.jpg

アカウント詳細画面を開くと、下の方に「鍵を追加」というメニューがあるので、そちらから秘密鍵(JSON)を作成します。

jsonを保存_censored.jpg

後にこの秘密鍵を使ってアプリからサービスアカウントへの認証を行うので、ダウンロードして保管しておきましょう。

Googleドライブの設定

https://drive.google.com/drive/u/0/my-drive

次にGoogleドライブの設定を行います。

フォルダを作成

test.png

テスト用に適当なフォルダを用意しましょう。

サービスアカウントを共同編集者に追加

共有.png
メルアド共有_censored.jpg

先ほど作成したサービスアカウントがGoogleドライブ内のフォルダにアクセスできるよう、「共有」を選択してサービスアカウントのメールアドレスを追加します。

メルアド_censored.jpg

(メールアドレスはサービスアカウントの詳細画面から確認可能)

共有が終わったら下準備は完了です。

コードを実装

さて、いよいよコードの実装を行っていきます。

Laravelプロジェクトを作成

誰でも同じ結果になるように、今回はLaravelプロジェクトを作成するところから始めます。

https://github.com/kazama1209/laravel-new

↑サンプルを用意したので、各自クローンして使ってください。

セットアップ

Dockerでちゃちゃっと準備を済ませます。

コンテナを起動
$ docker-compose up -d
コンテナ内に入る
$ docker-compose exec php bash
プロジェクトを作成
$ composer create-project laravel/laravel
ブラウザを確認

スクリーンショット 2020-12-15 1.52.52.png

「localhost」へアクセスしてLaravelの初期画面が表示されていれば成功。

Composerをインストール

https://github.com/googleapis/google-api-php-client

Google Drive APIを使用するために公式のライブラリを準備します。

$ cd laravel
$ composer require google/apiclient:^2.0

...

  - Downloading phpseclib/phpseclib (2.0.29)
  - Downloading psr/cache (1.0.1)
  - Downloading firebase/php-jwt (v5.2.0)
  - Downloading google/auth (v1.14.3)
  - Downloading google/apiclient-services (v0.156)
  - Downloading google/apiclient (v2.8.3)
  - Installing phpseclib/phpseclib (2.0.29): Extracting archive
  - Installing psr/cache (1.0.1): Extracting archive
  - Installing firebase/php-jwt (v5.2.0): Extracting archive
  - Installing google/auth (v1.14.3): Extracting archive
  - Installing google/apiclient-services (v0.156): Extracting archive
  - Installing google/apiclient (v2.8.3): Extracting archive
4 package suggestions were added by new dependencies, use `composer suggest` to see details.
Package phpunit/php-token-stream is abandoned, you should avoid using it. No replacement was suggested.
Generating optimized autoload files
> Illuminate\Foundation\ComposerScripts::postAutoloadDump
> @php artisan package:discover --ansi
Discovered Package: facade/ignition
Discovered Package: fideloper/proxy
Discovered Package: fruitcake/laravel-cors
Discovered Package: laravel/tinker
Discovered Package: nesbot/carbon
Discovered Package: nunomaduro/collision
Package manifest generated successfully.
68 packages you are using are looking for funding.
Use the `composer fund` command to find out more!

先ほど作成したプロジェクト内に移動し、Composerをインストール。

Googleドライブにファイルをアップロードするための関数を作成

$ mkdir app/Libs
$ touch GoogleDrive.php

今回はapp/配下に「Libs」というフォルダを用意し、「GoogleDrive.php」ファイル内に関数を作成していきます。

app/Libs/GoogleDrive.php
<?php

namespace App\Libs;

class GoogleDrive
{
    /**
     * Googleドライブへの認証を行う
     * @return Google_Service_Drive
     */
    public function getDriveClient(): \Google_Service_Drive
    {
        $client = new \Google_Client();

        // サービスアカウント作成時にダウンロードしたJSONファイルの名前を「client_secret」変更し、configフォルダ内に設置
        $client->setAuthConfig(config_path('client_secret.json'));
        $client->setScopes(['https://www.googleapis.com/auth/drive']);

        return new \Google_Service_Drive($client);
    }

    /**
     * ファイルをアップロードする
     *
     * @return GoogleDrive
     */
    public function fileUpload()
    {
        $driveClient = $this->getDriveClient();

        $fileMetadata = new \Google_Service_Drive_DriveFile([
            'name' => 'sample.jpg', // Googleドライブへアップロードされた際のファイル名(今回は「sample.jpg」とする)
            'parents' => ['xxxxxxxxxxxxxxxxxx'], // 保存先のフォルダID(配列で渡さなければならないので注意)
        ]);

        $driveClient->files->create($fileMetadata, [
            'data' => file_get_contents(storage_path('app/public/sample.jpg')), // アップロード対象となるファイルのパス(今回はstorage/app/public配下の「sample.jpg」を指定)
            'mimeType' => ' image/jpeg',
            'uploadType' => 'media',
            'fields' => 'id',
        ]);
    }
}

全体的にかなりシンプルな記述にしてあるので、細かい説明は割愛しても大丈夫だと思います。

※アップロードに使用する画像は各自用意してapp/Storage/app/public配下に設置してください。

Artisanコマンドを作成

動作確認しやすいようにArtisanコマンドを作成していきます。

$ php artisan make:command GoogleDriveFileUpload

ターミナルで↑を打ち込むとapp/Console/Commands/配下に「GoogleDriveFileUpload.php」というファイルが作成されているはずなので、次のように変更してください。

app/Console/Commands/GoogleDriveFileUpload.php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Libs\GoogleDrive;

class GoogleDriveFileUpload extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'google_drive:file_upload';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Upload file to Google Drive';

    /**
     * @var GoogleDrive
     */
    public $googleDrive;

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct(GoogleDrive $googleDrive)
    {
        parent::__construct();
        $this->googleDrive = $googleDrive;
    }

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        $this->googleDrive->fileUpload();
    }
}

Artisanコマンドを実行

$ php artisan google_drive:file_upload

アップロード成功.png

先ほど作成したArtisanコマンドを実行し、Googleドライブ内に「sample.jpg」というファイルがアップロードされていれば成功です。

あとがき

お疲れ様でした。基本的な流れは以上になります。あとは各自のプロジェクトに合わせてカスタマイズしてみてください。

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

【cakephp2】for文を使った複数レコードの更新

はじめに

cakephp2を使って複数レコードを更新した際にレコードがうまく更新されずハマってしまったので備忘録も兼ねて記事を作成します。
音声認識エンジンの開発を行っている企業でアルバイトをしており、普段はアノテーションツールやテストツールの新規機能開発や保守、モデルを効率よく学習させるためのあれこれなど、ML以外のことはなんでもやります。

ML用のツールを開発することが多いため普段はDjangoをメインに触っているのですが、中にはcakephp2で書かれたツールもあり、今回記事にするのはこちらのツールの改修をする際に学んだことです。

やりたいこと

今回実装したかった機能はシンプルで、Vue.js + TypeScriptで書かれたフロントエンドのアプリからcakephp2で実装してあるapiにリクエストを送って、テーブルAに一対多で紐づくテーブルBのレコードを複数更新するという機能でした。

元々あった機能として、テーブルBについて単体のレコードを更新できるエンドポイントは実装済みでした。
なので、パッと見た感じたと改修が必要なのはフロントエンドのみでバックエンドについては今まで使っていたエンドポイントに複数レコードを更新するパラメータを投げればいい感じに動いてくれるだろうと予想していました。

元々のコード

一部ですが、以下が元々実装されていたapiのコードになります。

BeforeController.php
public function save_a_and_b() {
    $this->loadModel('A');
    $this->loadModel('B');

    $update_param = $this->request->data('update_param');

    $a_records = json_decode($this->request->data('a_records'), true);

    try {
        $dataSource = $this->Region->getDataSource();
        $dataSource->begin();

        foreach ($a_records as $r) {
            $data = [
                'id' => Hash::get($r, 'id'),
                'update_param' => $update_param,
            ]
            $this->A->clear();
            $this->A->validator()...
            $savedA = $this->A->save($data);

            // Bの登録
            $this->B->deleteAll(['a_id' => $savedA['A']['id']]);
            if (bId = Hash::get($r, 'b_id')) {
                $this->B->save([
                    'a_id' => $savedA['A']['id'],
                    'b_id' => $bId,
                ])
            }
        }
    } catch(Exception $ex) {
        ....
    }
}

かなり簡略化したコードなのでご容赦ください。
問題のテーブルBの更新についてはforeach文中のif文の中で行っています。
当初より、複数レコードの更新、作成に対応できるように実装されていたのか、フロントの側からは配列の形で複数パラメータを投げ、バックではfor文で回してレコードの登録を行う実装になっていたようです。

意図しない挙動

当初の想像では上記のエンドポイントに複数レコードのパラメータを持つ配列を投げれば、テーブルA, Bに関して作成、更新が行えると予想していました。
paramsB_1をリクエストとして投げるとそれに対応したrecordB_1が作られると思ってください。

当初の予想
graph1.png

実際の結果
graph2.png

上の2つの図を見比べてみると、4つ分更新されるはずのテーブルBのレコードが実際には2つ分しか更新されていません。

解決策

次の一行を追加することで、4つのレコードとも更新されるようになりました。

AfterController.php
public function save_a_and_b() {
    $this->loadModel('A');
    $this->loadModel('B');

    $update_param = $this->request->data('update_param');

    $a_records = json_decode($this->request->data('a_records'), true);

    try {
        $dataSource = $this->Region->getDataSource();
        $dataSource->begin();

        foreach ($a_records as $r) {
            $data = [
                'id' => Hash::get($r, 'id'),
                'update_param' => $update_param,
            ]
            $this->A->clear();
            $this->A->validator()...
            $savedA = $this->A->save($data);

            // Bの登録
            $this->B->deleteAll(['a_id' => $savedA['A']['id']]);
            if (bId = Hash::get($r, 'b_id')) {
                $this->B->clear(); この行を追加
                $this->B->save([
                    'a_id' => $savedA['A']['id'],
                    'b_id' => $bId,
                ])
            }
        }
    } catch(Exception $ex) {
        ....
    }
}

clear()メソッド

公式Docでは次のような説明がされていました。

Model::clear()
このメソッドは、モデルの状態をリセットし、保存していないデータやバリデーションエラーを リセットするために使用します。

実際にcakephp2のソースコードを探してみると、clear()メソッドは次のような関数になっています。

Model.php
/**
 * This function is a convenient wrapper class to create(false) and, as the name suggests, clears the id, data, and validation errors.
 *
 * @return bool Always true upon success
 * @see Model::create()
 */
    public function clear() {
        $this->create(false);
        return true;
    }

どうやらcreate()メソッドをラップした関数のようです。
create()メソッドは次のようになっています。

Model.php
/**
 * Initializes the model for writing a new record, loading the default values
 * for those fields that are not defined in $data, and clearing previous validation errors.
 * Especially helpful for saving data in loops.
 *
 * @param bool|array $data Optional data array to assign to the model after it is created. If null or false,
 *   schema data defaults are not merged.
 * @param bool $filterKey If true, overwrites any primary key input with an empty value
 * @return array The current Model::data; after merging $data and/or defaults from database
 * @link https://book.cakephp.org/2.0/en/models/saving-your-data.html#model-create-array-data-array
 */
    public function create($data = array(), $filterKey = false) {
        $defaults = array();
        $this->id = false;
        $this->data = array();
        $this->validationErrors = array();

        if ($data !== null && $data !== false) {
            $schema = (array)$this->schema();
            foreach ($schema as $field => $properties) {
                if ($this->primaryKey !== $field && isset($properties['default']) && $properties['default'] !== '') {
                    $defaults[$field] = $properties['default'];
                }
            }

            $this->set($defaults);
            $this->set($data);
        }

        if ($filterKey) {
            $this->set($this->primaryKey, false);
        }

        return $this->data;
    }

コメント部を見ると、create()メソッドはレコードの書き込みをする際にデフォルト値を読み込んでモデルの初期化をするようです。ループ処理でレコードの保存をする際に活躍するとの記述もあります。

create()メソッドについて、公式Docには次のように書かれています。

Model::create(array $data = array())
このメソッドはデータを保存するためにモデルの状態をリセットします。 実際にはデータベースにデータは保存されませんが、Model::$idフィールドが クリアされ、データベースのフィールドのデフォルト値を元にModel::$data の値を セットします。データベースフィールドのデフォルト値が存在しない場合、 Model::$data には空の配列がセットされます。
$data パラメータ (上記で説明したような配列の形式) が渡されれば、 データベースフィールドのデフォルト値とマージされ、モデルのインスタンスは データを保存する準備ができます (データは $this->data でアクセスできます)。
$data パラメータへ false や null が渡された場合、 Model::$data には空の配列がセットされます。

create() vs clear()

create()は初期化を行うのに対して、clear()はid, dataとvalidation errorをリセットする。
デフォルト値が入ったフォーム入力などを行う場合はcreate()メソッドを使う方が好ましいと思われますが、基本的にclear()を使うようにすれば問題ないのかなと感じました。

まとめ

テーブルの複数レコード更新をする際にはsave()の前にclear()メソッドを使って、モデルの初期化をするようにする。
これにより、データのコンフリクトなどを防ぐことができる。

参考

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

【CakePHP2】for文を使った複数レコードの更新

はじめに

cakephp2を使って複数レコードを更新した際にレコードがうまく更新されずハマってしまったので備忘録も兼ねて記事を作成します。

筆者は現在、音声認識エンジンの開発を行っている企業でアルバイトをしており、普段はアノテーションツールやテストツールの新規機能開発や保守、モデルを効率よく学習させるためのあれこれなど、ML以外のことはなんでもやるといった業務内容です。

ML用のツールを開発することが多いため普段はDjangoをメインに触っているのですが、中にはcakephp2で書かれたツールもあり、今回記事にするのはこちらのツールの改修をする際にハマった内容になります。

やりたいこと

今回実装したかった機能はシンプルで、Vue.js + TypeScriptで書かれたフロントエンドのアプリからcakephp2で実装してあるapiにリクエストを送って、テーブルAに一対多で紐づくテーブルBのレコードを複数更新するという機能です。

元々あった機能として、テーブルBについて単体のレコードを更新できるエンドポイントは実装済みでした。
なので、パッと見た感じたと、改修が必要なのはフロントエンドのみでバックエンドについては今まで使っていたエンドポイントに複数レコードを更新するパラメータを投げればそのまま動いてくれるだろうと予想していました。

元々のコード

一部ですが、以下が元々実装されていたapiのコードになります。

BeforeController.php
public function save_a_and_b() {
    $this->loadModel('A');
    $this->loadModel('B');

    $update_param = $this->request->data('update_param');

    $a_records = json_decode($this->request->data('a_records'), true);

    try {
        $dataSource = $this->A->getDataSource();
        $dataSource->begin();

        foreach ($a_records as $r) {
            $data = [
                'id' => Hash::get($r, 'id'),
                'update_param' => $update_param,
            ]
            $this->A->clear();
            $this->A->validator()...
            $savedA = $this->A->save($data);

            // Bの登録
            $this->B->deleteAll(['a_id' => $savedA['A']['id']]);
            if (bId = Hash::get($r, 'b_id')) {
                $this->B->save([
                    'a_id' => $savedA['A']['id'],
                    'b_id' => $bId,
                ])
            }
        }
    } catch(Exception $ex) {
        ....
    }
}

かなり簡略化したコードなのでご容赦ください。
問題のテーブルBの更新についてはforeach文中のif文の中で行っています。
当初より複数レコードの更新、作成に対応できるように実装されていたのか、フロントの側からは配列の形で複数パラメータを投げ、バックではfor文で回してレコードの登録を行う実装になっていました。

意図しない挙動

当初の想像では上記のエンドポイントに複数レコードのパラメータを持つ配列を投げれば、テーブルA, Bに関して作成、更新が行えると予想していました。
paramsB_1をリクエストとして投げるとそれに対応したrecordB_1が作られると思ってください。

当初の予想
graph1.png

実際の結果
graph2.png

上の2つの図を見比べてみると、4つ分更新されるはずのテーブルBのレコードが実際には2つ分しか更新されていません。

解決策

次の一行を追加することで、4つのレコードとも更新されるようになりました。

AfterController.php
public function save_a_and_b() {
    $this->loadModel('A');
    $this->loadModel('B');

    $update_param = $this->request->data('update_param');

    $a_records = json_decode($this->request->data('a_records'), true);

    try {
        $dataSource = $this->Region->getDataSource();
        $dataSource->begin();

        foreach ($a_records as $r) {
            $data = [
                'id' => Hash::get($r, 'id'),
                'update_param' => $update_param,
            ]
            $this->A->clear();
            $this->A->validator()...
            $savedA = $this->A->save($data);

            // Bの登録
            $this->B->deleteAll(['a_id' => $savedA['A']['id']]);
            if (bId = Hash::get($r, 'b_id')) {

                $this->B->clear(); この行を追加

                $this->B->save([
                    'a_id' => $savedA['A']['id'],
                    'b_id' => $bId,
                ])
            }
        }
    } catch(Exception $ex) {
        ....
    }
}

clear()メソッド

公式Docでは次のような説明がされていました。

Model::clear()
このメソッドは、モデルの状態をリセットし、保存していないデータやバリデーションエラーを リセットするために使用します。

実際にcakephp2のソースコードを探してみると、clear()メソッドは次のような関数になっています。

Model.php
/**
 * This function is a convenient wrapper class to create(false) and, as the name suggests, clears the id, data, and validation errors.
 *
 * @return bool Always true upon success
 * @see Model::create()
 */
    public function clear() {
        $this->create(false);
        return true;
    }

どうやらcreate()メソッドをラップした関数のようです。
clear()元となるcreate()メソッドは次のようになっています。

Model.php
/**
 * Initializes the model for writing a new record, loading the default values
 * for those fields that are not defined in $data, and clearing previous validation errors.
 * Especially helpful for saving data in loops.
 *
 * @param bool|array $data Optional data array to assign to the model after it is created. If null or false,
 *   schema data defaults are not merged.
 * @param bool $filterKey If true, overwrites any primary key input with an empty value
 * @return array The current Model::data; after merging $data and/or defaults from database
 * @link https://book.cakephp.org/2.0/en/models/saving-your-data.html#model-create-array-data-array
 */
    public function create($data = array(), $filterKey = false) {
        $defaults = array();
        $this->id = false;
        $this->data = array();
        $this->validationErrors = array();

        if ($data !== null && $data !== false) {
            $schema = (array)$this->schema();
            foreach ($schema as $field => $properties) {
                if ($this->primaryKey !== $field && isset($properties['default']) && $properties['default'] !== '') {
                    $defaults[$field] = $properties['default'];
                }
            }

            $this->set($defaults);
            $this->set($data);
        }

        if ($filterKey) {
            $this->set($this->primaryKey, false);
        }

        return $this->data;
    }

コメント部を見ると、create()メソッドはレコードの書き込みをする際にデフォルト値を読み込んでモデルの初期化をするようです。ループ処理でレコードの保存をする際に活躍するとの記述もあります。

create()メソッドについて、公式Docには次のように書かれています。

Model::create(array $data = array())
このメソッドはデータを保存するためにモデルの状態をリセットします。 実際にはデータベースにデータは保存されませんが、Model::$idフィールドが クリアされ、データベースのフィールドのデフォルト値を元にModel::$data の値を セットします。データベースフィールドのデフォルト値が存在しない場合、 Model::$data には空の配列がセットされます。
$data パラメータ (上記で説明したような配列の形式) が渡されれば、 データベースフィールドのデフォルト値とマージされ、モデルのインスタンスは データを保存する準備ができます (データは $this->data でアクセスできます)。
$data パラメータへ false や null が渡された場合、 Model::$data には空の配列がセットされます。

create() vs clear()

create()は初期化を行うのに対して、clear()はid, dataとvalidation errorをリセットする。
デフォルト値が入ったフォーム入力などを行う場合はcreate()メソッドを使う方が好ましいと思われますが、基本的にclear()を使うようにすれば問題ないのかなと感じました。

まとめ

テーブルの複数レコード更新をする際にはsave()の前にclear()メソッドを使って、モデルの初期化をするようにする必要があるようです。
これにより、データのコンフリクト等を防ぐことができました。

参考

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