メモ帳

読むな

Laravel Eloquent ORM で ORDER BY を複数指定する

Laravel の Eloquent ORM で SELECT * FROM samples ORDER BY column1 DESC, column2 ASC みたいに ORDER BY を複数指定したい場合の書き方

<?php
App\Models\Sample::orderBy('column1', 'desc')->orderBy('column1', 'asc')->get();

もしくは

<?php
App\Models\Sample::orderByDesc('column1')->orderBy('column1')->get();

何故か毎回連想配列でできると思いこんでApp\Models\Sample::orderBy(['column1' => 'desc', 'column1' => 'asc'])->get();みたいに書いてはエラー→ググる→修正のループを繰り返してたけどこれで覚えるだろ。。。

参考

readouble.com

Vue.js でファイルアップロードをする

Vue.js というより axios でファイルアップロードをしたかった
保存ボタンがなく、アップロード時にサーバーに送るのを想定
サーバーサイドのFWは Laravel を想定

<template>
  <div>
    <!-- ファイルアップロード用の input -->
    <input
      type="file"
      ref="image"
      accept="image/*"
      name="image"
      @change="uploadImage"
    />
  </div>
</template>

<script>
import axios from "axios";

export default {
  name: "InputFile",
  methods: {
    uploadImage() {
      // parameter の用意
      const params = new FormData();
      params.append("image", this.$refs.image.files[0]);

      axios
        .post(`${process.env.MIX_APP_URL}ファイルアップロード先のURI`, params, {
          headers: {
            "Content-Type": "multipart/form-data"
            Authorization: "XXXXXXXXXXXX",
          },
        })
        .then((res) => {
          console.log(res, res.data);
          if (res.status !== 200) {
            console.error("画像のアップロードに失敗。");
            return;
          }
          // サーバー側のレスポンスに合わせてエラーハンドリングなど
          if (!res.data.succeeded) {
            console.error("画像のアップロードに失敗。");
            return;
          }
          // アップロード成功時の処理
          console.log("画像アップロードに成功。");
        })
        .catch((error) => {
          console.error(error, error.response);
          // Laravel のバリデーションエラーの場合のレスポンスに合わせてエラーメッセージを出力
          if (
            error.response.status === 422 &&
            error.response.data.errors.length !== 0
          ) {
            console.error(error.response.data.errors.image);
            return;
          }
          // その他のエラー
          console.error("画像のアップロードに失敗。");
          return;
        });
    },
  },
};
</script>

axios でファイルアップロードってやったことないからどうやるんだろうって思ってたけどよくよく考えたら FormData でパラメータをセットしてPOSTすればいいだけの話だった

参考

qiita.com

HTML と CSS のみでドラえもんを作る

(割とやりつくされたネタだから今更感すごいけど。。。)
[ドラえもん CSS] とかでググると実装方法がわかってしまうため今回は終始ググらないようにして実装

できたもの

f:id:IlIIlIIIlIIlI:20211010193053p:plain:w200

実装

感想

所詮丸と直線の組み合わせだし position とか使わんくても Flexbox だけでモダンなドラえもん作ってやるぜ!!!って思って手を出したけど意外とムズかった。position 使いまくって結局泥臭いコードになってる。(HTML構造のせいかもだけど)
完成後に他の方のドラえもんも見て回ったけど↓このドラえもんの完成度が圧倒的だった。体もあるし

shopdd.jp

後これも。
俺の想定してたドラえもんの口がまさにこれ qiita.com

おまけ

作成過程で生み出されたバケモンたちの供養

f:id:IlIIlIIIlIIlI:20211010165821p:plain:w200f:id:IlIIlIIIlIIlI:20211010165816p:plain:w200
f:id:IlIIlIIIlIIlI:20211010165814p:plain:w200f:id:IlIIlIIIlIIlI:20211010165812p:plain:w200



JSでブラウザの位置情報を取得する

(function () {
    if (!navigator.geolocation) {
        console.warn("Geolocation APIを利用できない環境");
        return;
    }
    navigator.geolocation.getCurrentPosition(
        (positions) => {
            // 位置情報の取得成功時の処理
            console.log(positions.coords.latitude, positions.coords.longitude);
        },
        (error) => {
            // 位置情報の取得失敗時の処理
            const errors = {
                1: '位置情報の利用が許可されていない。',
                2: '位置情報の取得に失敗。',
                3: 'タイムアウト。'
            };
            console.error(error.code, errors[error.code])
        },
        {
            enableHighAccuracy: true,
            timeout: 5000,
            maximumAge: 0,
        }
    );
}());

options詳細

option デフォルト 説明
enableHighAccuracy false 精度の高い位置情報を取得するか GPSを使うことができる場合に使う
timeout 無制限 タイムアウトまでの時間
maximumAge 0 取得済みの位置情報の有効期限

ブログ用に書いてるから可読性死んでるけどプロダクトに使うときはコールバック関数は別で定義しようね。。。

参考

developer.mozilla.org developer.mozilla.org developer.mozilla.org


[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

HTML5 Geolocation Bringing Location to W...
価格:1179円 (2021/10/9時点)


JSで任意の個数分の配列を作成する

PHPで言うところの range(1, 20) をやりたかったけどJSにはそれらしき機能がなく毎回調べているのでメモ

[...Array(5).keys()] // [0, 1, 2, 3, 4]
Array.from(Array(5).fill().keys()) // [0, 1, 2, 3, 4]

indexを0以外から始めたいときはmapで足してあげる

[...Array(5).keys()].map(row => row+3) // [3, 4, 5, 6, 7]

単純に個数分だけ作成したいときはArray(5).fill()でOK

配列を任意の個数で分割したい

array_chunk() みたいに全部均等ではなく, 3個, 2個, 2個, 6個, 1個, 1個 ... みたいに分割数が可変な配列を作りたかったときに地味にハマったのでメモ

<?php
$src = range(1, 15);
$chunks = [3, 2, 2, 6, 1, 1]; // 分割したい個数
$offset = 0;
$values = [];
foreach ($chunks as $chunk) {
    $values[] = array_slice($src, $offset, $chunk);
    $offset += $chunk;
}

結果

array(6) {
  [0]=>
  array(3) {
    [0]=>
    int(1)
    [1]=>
    int(2)
    [2]=>
    int(3)
  }
  [1]=>
  array(2) {
    [0]=>
    int(4)
    [1]=>
    int(5)
  }
  [2]=>
  array(2) {
    [0]=>
    int(6)
    [1]=>
    int(7)
  }
  [3]=>
  array(6) {
    [0]=>
    int(8)
    [1]=>
    int(9)
    [2]=>
    int(10)
    [3]=>
    int(11)
    [4]=>
    int(12)
    [5]=>
    int(13)
  }
  [4]=>
  array(1) {
    [0]=>
    int(14)
  }
  [5]=>
  array(1) {
    [0]=>
    int(15)
  }
}

ちなみにハマったとこは最初 $offset 使わずに $values[] = array_slice($src, count($values), $chunk); みたいな書き方してて、当たり前だけど offset が1個ずつしか増えてかんくて, 「なんで!??!?!?!?!」ってなってた🥰クソバカ🥰

参考

www.php.net

bashalog.c-brains.jp

Terminal 起動時に 「You have new mail.」 と表示された場合の対処法

f:id:IlIIlIIIlIIlI:20210926182834p:plain

ホストマシンに設定したテスト用の cron が失敗してた時にメールが届いてた
放置してたけど地味に鬱陶しかったので対処したときのメモ


$ mail
>U  1 # 未読のメールが件数分表示される
>N  2
>N  3 
>N  4 
? d*
? q
  1. mail コマンドで未読メールの一覧を表示
  2. 適当に件名で判断して全削除OKの場合はそのままd*
  3. qで終了

削除オプション

command 意味
d1 1だけ削除
d1 2 1と2を削除
d1-10 1~10を削除
d* 全削除

先頭のアルファベットの意味

alphabet 意味
N 新着
U 未読

参考

linux.just4fun.biz

pc-karuma.net

navymobile.co.jp

Laravel でQueryBuilderで組み立てられたSQLを確認する方法

QueryBuilder(or Eloquent ORM)で組み立てたSQLを確認したい時に使う
いずれも tinker で実施

①QueryBulderから確認する場合

>>> App\Models\User::where('id', 1)->toSql()
=> "select * from `users` where `id` = ?"

②複数のQueryBuilderから確認したい場合

>>> DB::enableQueryLog()  # ログの出力をONにする
=> null
>>> App\Models\User::find(1) # SQLを確認したい処理分だけ実行する
=> App\Models\User {#370
     id: 1,
     name: "example",
     email: "example@example.com",
   }
>>> DB::getQueryLog() # 実施した分のSQLが出力される
=> [
     [
       "query" => "select * from `users` where `users`.`id` = ? limit 1",
       "bindings" => [
         1,
       ],
       "time" => 2.76,
     ],
   ]
>>> DB::flushQueryLog() # ログの中身を空にする

適当なタイミングで DB::flushQueryLog() をすればログの中身が空になる
毎回空にするのであれば dump(DB::getQueryLog()) && DB::flushQueryLog() を実行すれば出力と同時にログの中身も空にしてくれる

参考

qiita.com

PHP で Selenium を動かすためにやったこと

Selenium 童貞捨てたのは Python だったけど使い慣れてる PHP でも Selenium を使いたかったので備忘録
以前やった内容を後日まとめてるのでもしかしたら手順に抜けがあるかも。。。

前提

  • composer はDL済み

手順

1. ChromeDriver のダウンロード

自分の使ってる GoogleChrome と同じversionの ChromeDriver をDLして解凍する chromedriver.chromium.org

2. chromedriver のパスを通す

自分の場合は /usr/local/bin に移動

mv ~/Downloads/chromedriver /usr/local/bin

※未検証だけど確か WebDriver で chromedriver のパスを指定できるようになってるはず

3. selenium-server-standalone のダウンロード

下記から適当に安定版をDL
www.selenium.dev 自分の場合はDL時にChromeのセキュリティ関係(?)でブロックされたのでシークレットモードで開いてDLした

4. selenium-server-standalone をホームディレクトリに移動

起動しやすい場所であればどこでもOK

mv ~/Downloads/selenium-server-standalone-xxxx.jar ~/

5. composer で php-webdriver/webdriver のインストール

$ composer require --dev php-webdriver/webdriver

6. 適当なプログラムの作成

example.php というファイル名で作成

<?php

use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\WebDriverBy;

require_once './vendor/autoload.php';

(function () {
    $host = 'http://localhost:4444/wd/hub';
    $driver = RemoteWebDriver::create($host, DesiredCapabilities::chrome());
    $driver->get('https://www.google.com/');
    $driver->findElement(WebDriverBy::cssSelector('input[name="q"]'))
        ->sendKeys('不死川実弥 過去 何巻')->submit();
})();

不死川実弥の過去が何巻だったかをググるだけのプログラム

7. selenium-server-standalone の起動

java -jar ~/selenium-server-standalone-3.4.0.jar &

8. プログラムの実行

php example.php

Macの場合、セキュリティ関係で初回は↓のようなポップアップが出てくるので、
f:id:IlIIlIIIlIIlI:20210926161927p:plain:w200

システム設定>セキュリティとプライバシー>一般タブで開くことを許可する必要がある
f:id:IlIIlIIIlIIlI:20210926162136p:plain

9. プログラムが動くことの確認


これで PHP でも自動操作し放題だぜ!!!! (対象サイトの利用規約を必ず読みましょう)
WebDriver の操作方法もいつかブログにまとめたい。いつか。。。!

参考

github.com blog.shimabox.net qiita.com


[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

【新品】鬼滅の刃 13 遷移変転 吾峠呼世晴/著
価格:484円(税込、送料別) (2021/9/26時点)


[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

鬼滅の刃コミックセット1-19巻 新品送料無料 ポイントでお得
価格:10900円(税込、送料無料) (2021/9/26時点)


USJ 行きましたメモ

※この記事にはUSJのアトラクションである 鬼滅の刃 XRライド や スーパー・ニンテンドー・ワールド に関するネタバレがあります。


知人の誕生日&鬼滅コラボを目当てにユニバ行ってきたので備忘録

エクスプレス・パスの購入

目当てが「鬼滅の刃 XRライド」だったから絶対待ち時間エグいだろうと思って事前に購入(購入にはClubユニバーサルへのアカウント登録が必要)
9/21のチケットを購入できるのが8/18~だったから8/18の0時になった瞬間にオンライン購入しようとしたけどアクセス集中のせいか、サイトが重くなって中々繋がらなかった
翌朝アクセスしたらみんなアクセスだけで購入はしないのかキャンセルしたのかわかんないけど、普通に繋がってスムーズに購入できた

当日行ったら最大240分待ちとかだったから本当に購入しておいてよかった。。。

入場券付きホテルの予約

前日にユニバから徒歩3分くらい?の入場券付きのプランでホテル泊まった


ホテル内ミニオンだらけで可愛かった🥰

f:id:IlIIlIIIlIIlI:20210925191010j:plain
f:id:IlIIlIIIlIIlI:20210925191014j:plain

入場待ち

AM9:00から入場可だったけど知人が禰豆子のポップコーンバケツが欲しいとのことなのでAM7:00くらいから一緒に入場待ち
ホテルの人曰く始発組がAM6:00くらいから並び始めるらしく、この時点ですでに結構並んでた
9月中頃だったけど日光強すぎたし日焼けしたくない人はまじで日傘は必須!!!!!

入場

AM9:00入場かと思いきやAM8:45から入場できた。入場直前に持ち物チェックがあったんだけどペットボトル1本は持ち込みOKらしい。

入場した途端ほとんどの人が禰豆子のポップコーンバケツの売り場までダッシュしてった
クルーの方が一生懸命止めてたけどみんなガン無視だったwww
ちな自分も軽く速歩きしたけど単純に体力がないのでついていけなかった。。。

売り場についたときには既に長蛇の列で2つくらい折返し部分できてた。
40分くらい?並んで禰豆子GET🎉(知人のついでに並んだだけなので親にあげた)
はちみつうめ味のポップコーンがついてくるんだけど禰豆子が目当てだからなのかポップコーンは断っている人もちらほらいた😢
汚したくない+持ち歩くには普通に邪魔wなので購入後はロッカーに預けました

並んでる最中はほんとに↓こんな感じだった。 クルーの方も割り込み禁止の呼びかけはしてたけど、足元にソーシャルディスタンスの線が貼ってあるわけでもないから列は結構密だった。

あとから知ったけど9時には完売してたらしい

アトラクションとか

体力的にエクスプレス・パスに含まれるアトラクションしか乗らなかったw
アトラクションで一番良かったのは圧倒的に 鬼滅の刃 XRライド だった🙌映像が綺麗すぎた🙌

入り口にある炭治郎と煉獄さん🔥

f:id:IlIIlIIIlIIlI:20210925190638j:plain

後ろに回ると禰豆子が顔出してて可愛かった🥰

f:id:IlIIlIIIlIIlI:20210925190812j:plain

マリオカートも期待してたんだけど3Dメガネ?が結構雑で期待はずれでした😭(雑というか横から現実が見える感じ)
ヨッシー・アドベンチャーはまじで子供向け。ヨッシーは可愛いけど個人的に一番いらんかったw
でもニンテンドー・ワールドの世界観はパークの中でずば抜けてよかった🤟

f:id:IlIIlIIIlIIlI:20210925192056j:plain
個人的にスマブラコクッパの爆弾?があることに一番テンションあがった(コクッパ詳しくない) f:id:IlIIlIIIlIIlI:20210925192122j:plain f:id:IlIIlIIIlIIlI:20210925191456j:plain
f:id:IlIIlIIIlIIlI:20210925191127j:plain

右上に現実が見える😂www

www.usj.co.jp

↑のエクスプレス・パスだったんだけど最後の方疲れ切っててミニオンだけ捨てたw
今考えたらヨッシー・アドベンチャー捨てればよかった。。。

時間的な余裕はあったんだけどまじで体力がついてかんかった😭ババア悲しい😭😭😭

ちなみにニンテンドー・ワールドは再入場不可になってて、エリア内のグッズショップにあるグッズのほとんどはエリア外にも売ってるんだけど、ほんの一部だけエリア内にしかないから迷ったらここで先に購入しておくのがおすすめ💪

キノピオカフェのご飯たち🍄 ご飯全部可愛かった🍄 f:id:IlIIlIIIlIIlI:20210925190358j:plain

夜ご飯は鬼滅の藤の花の食事処行った🎴
宇髄さんマーク🎆

f:id:IlIIlIIIlIIlI:20210925191746j:plain

持っていってよかったもの

  • 日傘 ... 日焼け防止のため。これがないと待ち時間暑すぎて死ぬ。
  • ハンディ扇風機 ... ないと暑すぎて死ぬ。
  • モバイルバッテリー ... 基本スマホ触ってなかったんだけど、やっぱ写真撮ってるからか結構減りが早くて持っていってよかった
  • 替えマスク ... コロナ対策。汗かいて気持ち悪いので一応持っていった
  • 持ち歩き除菌スプレー(ジェル) ... コロナ対策。至るところにおいてあったんだけど食べる直前に使う用

持っていかなくてよかったもの

  • USJ公式アプリ ... (持ち物じゃないけど) マップ確認とかアトラクションの待ち時間が確認できる

公式アプリについてはマップ確認できるんだけどマップに載ってる情報が少なすぎて全然使いもんにならんかったw 現在地が確認できるのは便利なんだけど、マップ上にエリア名?とかが載ってないから結構使いにくい
結局パーク内にある紙のマップの方使ってた
エクスプレス・パス使わずに行く人は待ち時間確認用にいいかも?


[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

劇場版「鬼滅の刃」無限列車編【通常版】【Blu-ray】 [ 花江夏樹 ]
価格:3741円(税込、送料無料) (2021/9/25時点)


[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

スーパーマリオ 3Dワールド + フューリーワールド
価格:5920円(税込、送料無料) (2021/9/25時点)


Composer のパッケージを作成した🎉

前からやってみたいと思いつつ後回しにしていた。。。
文字列をLeet文字に変換するっていう使い所がないパッケージだけど作れたことに満足:)

<?php
use Leet\Leet;
Leet::generate('shinazugawa sanemi') // 5h1n42u64w4 54n3m1

今は数字への変換しかできないけど記号への変換も今後対応する予定

そして Travis CI 童貞をやっと捨てた
いまいち使い方をよくわからんまま使ってるけどテスト通ってるのでヨシ!

f:id:IlIIlIIIlIIlI:20210918192311j:plain packagist.org



Laravel の Eloquent ORM でサブクエリを作る

日付単位で number の合計が10以上のレコードのみを抽出する

データ

id target_date number
1 2021-09-01 1
2 2021-09-02 2
3 2021-09-01 3
4 2021-09-03 4
5 2021-09-04 4
6 2021-09-11 2
7 2021-09-06 5
8 2021-09-12 23
9 2021-09-12 10
10 2021-09-23 20

SQL

SELECT
    *
FROM
    samples
INNER JOIN(
    SELECT
        id,
        SUM(number) AS number_sum
    FROM
        samples
    GROUP BY
        samples.target_date
) AS samples1
ON
    samples1.id = samples.id
WHERE
    number_sum >= 10;

※numberはMySQL予約語かも

これを Laravel の Eloquent を使ってサブクエリで書く

<?php
$subQuery = Sample::selectRaw('sum(number) as number_sum')
    ->addSelect('id')
    ->groupBy('target_date');

Sample::joinSub($subQuery, 'samples1', 'samples1.id', 'samples.id')
    ->where([['number_sum', '>=', '10']])
    ->get(['samples.id', 'target_date', 'number_sum']);

joinSub は Eloquent じゃなくて QueryBuilder の機能だった

結果

id target_date number_sum
8 2021-09-12 33
10 2021-09-23 20

参考

github.com

テーブルの中で重複しているレコードを抽出する

マスターデータ作るときに何のデータが重複してるかとかの確認したい時にいつもググってるので整理しておく
ただの確認用なのでサブクエリとかは使わない

テストデータ

id character_name
1 炭治郎
2 伊之助
3 善逸
4 カナヲ
5 禰豆子
6 炭治郎
7 禰豆子

炭治郎と禰豆子が重複しているからこの2行のみを抽出したい

SELECT
    character_name
FROM
    characters
GROUP BY
    character_name
HAVING
    COUNT(character_name) > 1;

結果

id character_name
1 炭治郎
5 禰豆子

要点

GROUP BY した結果に重複している行があるのを確認したいのでWHERE句ではなくHAVINGでやる必要がある
詳しくは以下に記載 iliiliiiliili.hatenablog.jp


[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

鬼滅の刃 23 (ジャンプコミックス) [ 吾峠 呼世晴 ]
価格:506円(税込、送料無料) (2021/9/25時点)



Seleniumでタイピング

前提

  • Seleniumを動かすための設定などは済んでいる
  • jpn.traineddata はDL済み
  • いきなり未定義変数が出てきたりしていますがあくまで個人メモです

手順

1. 初期起動画面待機

初期起動画面でコンテンツが表示されるのを待つ

driver.get(URL)
# 5秒待機
time.sleep(5)

2. 画面上の特定のボタンを押す

クリック座標が確認できるようにブラウザのConsoleに以下を貼り付けて、ボタンをクリックする

document.body.addEventListener('click', (e) => console.log(e))

ボタンをクリックするように設定

# ボタンをクリックしたときにConsoleに表示される座標を設定
ActionChains(driver).move_by_offset(x, y).click().perform()
# クリック後のアニメーションの待機
time.sleep(1)

3. 要素をスクショする

# canvas要素をスクショ
canvas = driver.find_element_by_tag_name('canvas').screenshot_as_png
# 一時保存
file_name = f'{cur_dir}/images/elm.png'
# 書き込みできるように
with open(file_name, 'wb') as f:
f.write(canvas)

4. スクショした画像から文字周辺をトリミング

# スクショしたcanvas要素から文字周辺のみをトリミング
src = Image.open(file_name)
crop = src.crop((left, upper, right, lower))
# トリミングした画像を保存
cropped_file = f'{cur_dir}/images/elm_cropped.png'
crop.save(cropped_file)

5. 文字認識の精度のための画像の調整

# 文字認識の精度調整のため画像を2値化して保存
image_read = cv2.imread(cropped_file)
image_read = cv2.bitwise_not(image_read)
bit_image_file = f'{cur_dir}/images/elm_bit.png'
cv2.imwrite(bit_image_file, image_read)

6. 文字認識

# 文字認識
tools = pyocr.get_available_tools()
res = tools[0].image_to_string(Image.open(bit_image_file), lang="eng")

7. タイピング

# 認識した文字をcliに出力してタイピング
print(res)
driver.find_element_by_tag_name('body').send_keys(res)

[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

実践Selenium WebDriver [ サタヤ・アバサララ ]
価格:3300円(税込、送料無料) (2021/9/25時点)


CodeIgniter3 の QueryBuilder でサブクエリを作る

databaseの接続設定は済んでいる前提

$subQuery = $this->db->select('count(*) as column_count, sample_id')->get_compiled_select('table_a');

$this->db->select('*')->join("({$subQuery}) AS table_a", 'table_a.sample_id = samples.id', 'left', false);
$query = $this->db->get('samples');
  • get() ではなく、get_compiled_select() を使用することでクエリを実行しない。
  • get_compiled_select() の第2引数がデフォルト true なので $this->db->reset_query() を実施してくれる。(正しくは _reset_select() )
  • $this->db->from('table') でやる場合はサブクエリの後に $this->db->reset_query()を呼んであげる必要がある。

個人的にはget_compiled_select()で済ませる方がシンプルなので好きです。


※   上記コードはサブクエリとの join() メソッドで第4引数のエスケープを false にしているので場合によってはSQLインジェクションが発生します。   サブクエリでjoinしてあげるクエリはあくまで CodeIgniter の QueryBuilder で組み立てた文字列を使用するようにしましょう!


参考記事

codeigniter.jp www.kabanoki.net