Doctrine SQL Filterでマルチテナントを実現する

こんにちは、岡野です。

最近マルチテナントのシステムを開発することがあり、共通のDBをテナントごとに管理する方法を調べました。

結論

Doctrineの以下機能を使用するとCRUDの各SQLを改変でき、テナントを透過的に制御可能(すなわち毎回WHERE句にテナントの条件を書かなくて良い)。 但し、@OneToOneは期待通りに動作しないため要注意。

環境

Doctrine 2.7.4
Symfony 4.4.16

SELECTの調査結果

以下の様なSQL Filterを登録。
class TenantFilter extends SQLFilter
{
    public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
    {
        // 100は実際には可変(後述)
        return $targetTableAlias.'.tenant_id = 100';
    }
}

単純なSELECT

$em->find(Parent::class, 1);
→ SELECT ... FROM parent WHERE id = 1 AND tenant_id = 100;

@OneToMany

$em->find(Parent::class, 1)->getChilds();
→ SELECT ... FROM parent WHERE id = 1 AND tenant_id = 100;
→ SELECT ... FROM child WHERE parent_id = 1 AND tenant_id = 100;

@ManyToOne

@OneToMany同様。

@ManyToMany

@OneToMany同様。

@OneToOne

$em->find(Example1::class, 1)->getExample2();
→ SELECT ... FROM example1 e1 LEFT JOIN example2 e2 ON e2.example1_id = e1.id WHERE e1.id = 1 AND e1.tenant_id = 100;
⇒ example2テーブルへのtenant_id = 100が存在しない

join()

$em->createQueryBuilder('c')->join('c.parent')->where('c.id = 1');
→ SELECT ... FROM child c JOIN parent p ON c.parent_id = p.id AND p.tenant_id = 100 WHERE c.id = 1 AND c.tenant_id = 100;

leftJoin()

join()同様。



INSERT/UPDATE/DELETEの調査結果

以下の様なDoctrine Event Listenerを登録。
class TenantListener
{
    public function prePersist(LifecycleEventArgs $args): void
    {
        assert($args->getEntity()->getTenant() === null);
        // 100は実際には可変
        $args->getEntity()->setTenant($this-em->find(Tenant::class, 100));
    }
    public function preUpdate(LifecycleEventArgs $args): void
    {
        // 安全のため他テナントを操作していないかチェックする
        if ($args->getEntity()->getTenant()->getId() !== 100) {
            throw new \LogicException();
        }
    }
    public function preRemove(LifecycleEventArgs $args): void
    {
        // preUpdate()同様
    }
}

INSERT

$em->persist(new Example());
→ INSERT INTO example (..., tenant_id) VALUES (..., 100);


その他

上記のようにDoctrine FilterのaddFilterConstraint()内でtenant_idを直接設定するとDoctrine Query CacheによりSQLが固定化されてしまい期待通りに動作しない。そのため以下の通りparameterを経由する必要があった。

// 外部から呼ぶ(RequestListenerなどから)
public function setTenantId(int $tenantId): void
{
    $this->setParameter('tenant_id', $tenantId, Types::INTEGER);
}

public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
{
    $tenantId = (int)$this->unquoteParameterValue($this->getParameter('tenant_id'));
    return $targetTableAlias.'.tenant_id = '.$tenantId;
}

private function unquoteParameterValue(string $value): string
{
    $len = strlen($value);
    if ($value[0] !== "'" || $value[$len - 1] !== "'") {
        throw new \LogicException($value);
    }
    return str_replace(['\\"', '\\\\'], ['"', '\\'], substr($value, 1, $len - 2));
}

Vue.jsでそれっぽい何かを作る2(改造)

株式会社イメージ・マジックの技術ブログ、今回の担当のsoenoです。 実務でいろいろ触ってはいるのですが。投稿しやすいコンテンツとなるとどうにも…。 というわけで以前の投稿に乗っかろうと思います。


何をするか

以前の投稿で作成したVueの機能を流用し、改造、改善していきます。

何を変えるか考える

前の機能の印象から変更点を考える

  1. webなのに白黒ってどうなのだろう。
    • 色を増やす
    • 色を増やせるように色の選択機能を追加 
  2. 見本と入力エリアが同じ大きさってどうなのだろう。(比較はしやすい?)
    • サイズを変える
  3. 升目少ない。
    • 増やす。
  4. どこ操作していいかわからない。
    • 入力エリアに枠を付けて目立たせる
  5. そろったかどうかどう確認するかわかりにくい(今は自動で勝手にチェックする)
    • チェックボタンを押すまでは比較しない
  6. 大人よりは子供向けのゲームになるだろうな。
    • ひらがなにする。

結果

できました!
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>vue</title>
    <style type="text/css">
      .wrapStyle{
          margin: 0 50px;
          display: block;
          position: relative;
          float: left;
      }
      .celAreaWrap{
        border: solid 2px #414141;
      }
      .celWrap{
          display: inline-block;
          box-sizing: content-box;
          margin:0;
          display: inline-block;
      }
      .cel{
          border:solid thin #eeeeee; 
          box-sizing: border-box;
          display: inline-block;
          margin:0;
          padding:0;
      }
      .colorCel{
        border: solid 2px #666666;
      }
      .btn{
        margin-top: 50px;
        border: solid 2px #666666;
        padding: 15px;
        min-width: 121px;
        text-align: center;
        background: #ffa500;
      }
      .cel_clr0{
          background-color:#ffffff;
      }
      .cel_clr1{
          background-color:#111111;
      }
      .cel_clr2{
          background-color:#ff0000;
      }
      .cel_clr3{
          background-color:#fc5000;
      }
      .cel_clr4{
          background-color:#f8ab04;
      }
      .cel_clr5{
          background-color:#48ff00;
      }
      .cel_clr6{
          background-color:#09f7ff;
      }
      .cel_clr7{
          background-color:#002fff;
      }
      .cel_clr8{
          background-color:#a304ff;
      }
    </style>
</head>
<body>
<div id="app">
  <div class="wrapStyle" v-bind:style="mihonBoxStyle">
    <p>みほん</p>
    <div v-for="(items, yoko) in resultList" v-bind:style="mihonCelWrap">
      <div v-for="(item, tate) in items" 
            v-bind:style="mihonCelStyle" class="cel" 
            v-bind:class="'cel_clr' + resultList[yoko][tate]">
      </div> 
    </div>
  </div>
  <div class="wrapStyle" v-bind:style="boxStyle">
    <p>みほんをまねしてね</p>
    <div class="celAreaWrap">
      <div v-for="(items, yoko) in userList" v-bind:style="celWrap">
        <div v-for="(item, tate) in items"
            v-on:click="changeCel(userList[yoko][tate])" :data-yoko="yoko" :data-tate="tate"
            v-bind:style="celStyle" class="cel"
            v-bind:class="'cel_clr' + userList[yoko][tate]">
        </div>
      </div>
    </div>
    <div>
      <p>いろをえらぼう</p>
      <div v-bind:style="celWrap">
        <div v-for="id in colorIdList" v-on:click="changeColor(id)" v-bind:style="celStyle" class="cel colorCel" v-bind:class="'cel_clr' + id">
        </div>
      </div>
    </div>
  </div>
  <div class="wrapStyle">
      <div v-on:click="check()" class="btn">そろったかな?</div>
  </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
<script language="javascript">
new Vue({
  el: '#app',
  data: {
    cellSize:60,
    nowColor:1,
    resultList: [
      [0,0,1,1,1,1,1,0,0],
      [0,1,4,4,4,4,4,1,0],
      [1,4,4,1,4,1,4,4,1],
      [1,4,4,4,4,4,4,4,1],
      [1,4,1,4,4,4,1,4,1],
      [1,4,1,4,4,4,1,4,1],
      [1,4,4,1,1,1,4,4,1],
      [0,1,4,4,4,4,4,1,0],
      [0,0,1,1,1,1,1,0,0],
    ],
    userList: [
      [0,0,0,0,0,0,0,0,0],
      [0,0,0,0,0,0,0,0,0],
      [0,0,0,0,0,0,0,0,0],
      [0,0,0,0,0,0,0,0,0],
      [0,0,0,0,0,0,0,0,0],
      [0,0,0,0,0,0,0,0,0],
      [0,0,0,0,0,0,0,0,0],
      [0,0,0,0,0,0,0,0,0],
      [0,0,0,0,0,0,0,0,0],
    ],
    
    colorIdList:[0,1,2,3,4,5,6,7,8],
    
    mihonBoxStyle:{
      width:0,
    },
    mihonCelWrap:{
      height:0,
    },
    mihonCelStyle:{
      width:0,
      height:0,
    },

    boxStyle:{
      width:0,
    },
    celWrap:{
      height:0,
    },
    celStyle:{
      width:0,
      height:0,
    },
  },
  created() {
    this.mihonCelStyle.width = this.cellSize/3+ 'px';
    this.mihonCelStyle.height = this.cellSize/3+ 'px';
    this.mihonCelWrap.height = this.cellSize/3+ 'px';
    this.mihonBoxStyle.width = this.mihonCelWrap * this.resultList[0].length + 'px';

    this.celStyle.width = this.cellSize+ 'px';
    this.celStyle.height = this.cellSize+ 'px';
    this.celWrap.height = this.cellSize+ 'px';
    this.boxStyle.width = this.cellSize * this.resultList[0].length + 4 + 'px';//4はボーダー
  },
  
  methods: {
    changeColor(id) {
      this.nowColor = id;
    },
    changeCel() {
      const yoko = parseInt(event.currentTarget.dataset.yoko);
      const tate = parseInt(event.currentTarget.dataset.tate);
      Vue.set(this.userList[yoko], tate, this.nowColor);
    },
    //確認ボタンが押されたら揃ったか確認する。
    check() {
      for(let i=0,len = this.resultList.length; i<len; i++){
        for(let ii=0,len2 = this.resultList[i].length; ii<len2; ii++){
          if(this.userList[i][ii] !== this.resultList[i][ii]){
            //不一致があったら処理を抜ける
            alert("まだそろってません。");
            return;
          }
        }
      }
      setTimeout(function () {
        alert("そろいました。おめでとうございます!");
      }, 100);
    },
  },
})
</script>
</body>
</html>

puppeteerでスクレイピングしてブログの投稿を検知する

こんにちは、イメージ・マジックの池田です。
今回はタイトルにもある通り、puppeteerのスクレイピング(とついでにブログの投稿検知)についてやっていきます。なぜなら最近プライベートで触ったからという、とても個人的な理由です。

puppeteerとは?

puppeteerというのは、Node.jsを用いてスクレイピングをするライブラリです。
はい、死ぬほど雑な説明をしました。違うわ!とまではいかないまでもお叱りを受けそうですね。詳しくは公式サイトをご覧ください。公式の英語サイトに投げる暴挙。とにかくNode.jsでスクレイピングのできるライブラリです。しかもChromeを作っているGoogle製。使わない理由はありませんね。

どうやって使うの?

  1. Node.jsの最新版をインストールします
  2. 好きな場所にフォルダを作り、npm init -yと打ち、npm i puppeteerします
用意はこれだけです。試しに公式サイトに載っているコードを実行してみましょう。同じフォルダにexample.jsを作成し、以下のコードを打ち込み、PowerShellなどからnode example.jsで実行してみてください。
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({ path: 'example.png' });

  await browser.close();
})();
同じフォルダにexample.pngというファイルが作成されたと思います。ファイルには見知らぬサイトのスクリーンショットが。puppeteerがブラウザを画面に出したりせずに撮ってきてくれたスクリーンショットです。実際にこのページにアクセスしてみると、画像そのままのサイトが表示されたと思います。
これを応用することで、ブログの投稿を検知することが可能となります。RSSがあればこういうことをしなくても大丈夫だったりしますが、そもそもRSSが下火だったり、RSSを提供するようなサイトじゃなかったりもしますからね。

プログラムの流れ

前提条件:昨日や一週間前に取ってきたタイトル一覧を手元に持っている(持っていなければ、下記手順の1~2、5だけを行います)
  1. ブログにアクセスする
  2. タイトル一覧を取得する
  3. 手元で持っているタイトル一覧と照合する
  4. 手元にないタイトルがあれば、そのタイトルを通知する
  5. 最新のタイトル一覧を手元に保存する
  6. おわり
簡単ですね。これをTypeScriptで書いたコードが下記です。私はVisual Studio Codeと組み合わせて使うTypeScriptが大好きなので、本来ならばこれの宣伝もしたいのですが、TypeScriptの素晴らしさは恐らく全ての生物が既に知っているので、ここでは取り扱いません。
import puppeteer, { Page } from "puppeteer";
import { readFileSync, writeFileSync } from "fs";
import csvParse = require("csv-parse/lib/sync")
import csvStringify = require("csv-stringify/lib/sync")
import { difference } from "./setUtils";

const CURRENT_FILE_NAME = 'current.csv';
const TITLE_SELECTOR = '.entry-title';

const readCurrentCsv = (): Set<string> | null => {
  try {
    const file = readFileSync(CURRENT_FILE_NAME);
    return new Set((csvParse(file) as Array<Array<string>>).flat());
  } catch (error) {
    // Error NO ENTryらしいです
    if (error.code === 'ENOENT') {
      return null;
    } else {
      throw new Error("知らんエラー。君誰?");
    }
  }
}

const fetchTitles = async (page: Page): Promise<Set<string>> => {
  const elements = await page.$$(TITLE_SELECTOR);
  return new Set(await Promise.all(elements.map(async ele => (await ele.getProperty('textContent'))!.jsonValue())) as Array<string>);
}

const writeCsv = (newTitles: Set<string>) => {
  writeFileSync(CURRENT_FILE_NAME, csvStringify([...newTitles].map(title => [title])));
}

const notify = (diffTitles: Set<string>) => {
  console.info(`
「${[...diffTitles].join('、')}」が開発者ブログに投稿されました。見てみましょう!
https://techblog.imagemagic.jp/
`.trim());
  // みたいのを通知したりしたいよね
}

const main = async (page: Page) => {
  const currentTitles = readCurrentCsv();
  const newTitles = await fetchTitles(page);

  if (currentTitles === null) {
    writeCsv(newTitles);
    return;
  }

  const diffTitles = difference(newTitles, currentTitles);
  if (!diffTitles.size) {
    return;
  }

  notify(diffTitles);
  writeCsv(newTitles);
}

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://techblog.imagemagic.jp/');

  try {
    await main(page);
  } finally {
    await browser.close();
  }
})();
main関数の中身を見れば、先程のプログラムの流れと大体同じなことがわかると思います。このプログラムをたとえばAWSのLambdaに乗せて、CloudWatchで定期的に叩くなどすれば、差分検知通知システムの完成です。
細かいコードの解説は省きます。皆さんも興味のあるサイト(たとえば本テックブログ!)をスクレイピングしたりして、実際に触ってみてください。

※スクレイピングは少し油断するとアクセス負荷を相手のサーバーにかけてしまったり、そもそもスクレイピングを許可していないサイトもあります。使い方を間違えれば違法行為です。この辺りのサイトを見つつ、用法用量を守って正しくお使いください。

Markdownについて

はじめに

Suzukiです。
今回はmarkdownについて書きます。

Markdown記法とは

Markdown記法とは軽量マークアップ言語の1つです。
記法については以下の公式ドキュメントを参考にしてください。
参考:mermaid公式ドキュメント -> https://mermaid-js.github.io/mermaid/#/

図表を描くならmermaid

簡単な図を表すのに便利です。
試しに書いてみると、   日本の地方別人口の円グラフ
```mermaid
pie title 日本の地域別人口(万人)(平成 22 年)
    "関東" : 4175
    "近畿" : 2250
    "中部" : 2157
    "九州" : 1467
    "東北" : 942
    "中国" : 758
    "北海道" : 552
    "四国" : 404
```
というような円グラフが描けます。
ライブエディタは以下にあります。
参考:Mermaid Live Editor -> https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ3JhcGggVERcbiAgICBBW0NocmlzdG1hc10gLS0-fEdldCBtb25leXwgQihHbyBzaG9wcGluZylcbiAgICBCIC0tPiBDe0xldCBtZSB0aGlua31cbiAgICBDIC0tPnxPbmV8IERbTGFwdG9wXVxuICAgIEMgLS0-fFR3b3wgRVtpUGhvbmVdXG4gICAgQyAtLT58VGhyZWV8IEZbZmE6ZmEtY2FyIENhcl0iLCJtZXJtYWlkIjp7fSwidXBkYXRlRWRpdG9yIjpmYWxzZX0
 

メリット

HTMLで書くより簡単に記述できます。
またHTMLのタグも使用できるので便利です。
PDFに変換することで配布しやすくなります。

実際の使用法

markdownで文章を書いてmermaidで図を描きこみ、PDFに変換して配りやすくして使っています。
最近では、業務の作業手順書をmarkdownで書きPDFに変換して関係部署に配布しました。

余談

実は今書いているのもmarkdownエディタを使って書いています(笑)
右にプレビューを出しながら書くと、見栄えを確認しながら書けるので便利ですね。

キーマクロを作ってみた話

こんにちは。イメージマジック三浦です。昼食のために外に出たある日、風から感じる香りが変わったなと感じます。もうすぐ春ですね。
今回はnginxのログを解析するために、サクラエディタのキーマクロを作った話です。

キーマクロを作った経緯

システムのパフォーマンスが悪い時期があり、ボトルネック調査の一環でnginxのログを解析していました。 解析時はExcelやGoogleスプレッドシートでフィルタリングできるように、テキスト置換を繰り返してきましたが、次第にパターンが決まってきて、置換自体を手動リプレイできる状態になってきました。そしてはたと気づきました。
 

ここまでパターン化できているならキーマクロで自動化しよう!

キーマクロの記録方法

ここで、キーマクロの記録について触れておきます。
キーマクロは「ツール」>「キーマクロの記録開始」とクリックすることで、記録開始できます。タイトルバーに「キーマクロの記録中」と出てきます。
上の状態で「ツール」>「キーマクロの記録終了」とクリックすると、キーマクロ記録を終了できます。

簡単なキーマクロを作ってみる

まずは簡単なキーマクロを作ってみます。

(1,2,3,4,5)
を正規表現置換で
(1)
(2)
(3)
(4)
(5)
のようにする操作をキーマクロで保存してみます。 正規表現は以下の通りです。
「ツール」>「キーマクロの読込」とすると、以下のファイルが出てきます。
このパス内にある「RecKey.mac」が、先ほど記録したキーマクロの実体ファイルです。このファイルをデスクトップなどにコピーします(元の場所におくと次回のキーマクロ記録で消えてしまうため)。

キーマクロ機能拡張

先のキーマクロの中身はこのようになっています。
S_ReplaceAll(',', '\\)\\r\\n\\(', 1068);  // すべて置換
S_ReDraw(0);    // 再描画
Excelできれいに貼り付けられるようにしたいという目的を踏まえ、キーマクロを拡張していきます。今回は以下のような方針で拡張しました。
  • S_ReplaceAll のパターンを増やして、異なる方法での置換を実行する
  • S_ReDrow(0)を最後に呼び出すことで、画面再描画は最小限にする
  • 1行をタブで区切られた時の要素数が均一かつ、Excelに貼り付けた時に情報がきれいに出る。
拡張したキーマクロの一部を紹介します。
// HTTPメソッドの分割
S_ReplaceAll('\"(GET|POST|HEAD) ', '\t$1\t', 1068);
// HTTP Status の分割
S_ReplaceAll(' ([2-5][0-9][0-9]) ', '\t$1\t', 1068);
// 再描画
S_ReDraw(0);
拡張したキーマクロは、共通設定の「マクロ」タブで登録し、メニューから実行できるようにします。そこから先は置換結果やExcelに貼りつけた結果を確認しながら、正規表現を調整していきます。

仕上げ

Excelへの貼り付けを繰り返すうちに、以下の点も入れたいという気持ちが出てきたので、拡張中のキーマクロに組み込みます。
  • 先頭に項目行を入れたい
  • 置換後は保存しておいて開いたらすぐに貼り付けられるようにしたい
コード自体はキーマクロの記録で生成されたものをコピペします。
仕上げまで実施し、最終的には以下の通りになりました。
// ファイルの先頭に移動
S_GoFileTop(0);
// 項目行を追加
S_InsText(′項目1\t項目2\…');

// HTTPメソッドの分割
S_ReplaceAll('\"(GET|POST|HEAD) ', '\t$1\t', 1068);
// HTTP Status の分割
S_ReplaceAll(' ([2-5][0-9][0-9]) ', '\t$1\t', 1068);
(他いろいろ)
// 再描画
S_ReDraw(0);

// 保存
FileSave();
これで、アクセスログを整形しExcelに貼り付ける直前まで整形する操作のキーマクロができました。作ってみると非常に便利で、心の中でドヤ顔しています。

最後に

冒頭に記載したボトルネックについては、部員の皆様の活躍により、現在は改善されています。

UWPアプリからOpenCVしてみる(その2)

矢野です。UWPアプリからOpenCVを使う話の決着をつけておきます。 Visual Studioを起動して、メインページにボタンを追加します。 
NuGetを使って、OpenCvSharp(OpenCvSharp4, OpenCvSharp4.runtime.uwp)をインストールします。 ボタンのクリックイベントハンドラ(find_Click)に、カメラからのキャプチャ、データ変換、ARマーカーの検出、結果を表示という流れでコードを書いていきます。
private async void find_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
    ImageEncodingProperties encProp = ImageEncodingProperties.CreateBmp();

    var source = captureElement.Source;

    SoftwareBitmap bitmap = null;
    using (var captureStream = new InMemoryRandomAccessStream())
    {
        await source.CapturePhotoToStreamAsync(encProp, captureStream);
        await captureStream.FlushAsync();
        captureStream.Seek(0);

        var decoder = await BitmapDecoder.CreateAsync(captureStream);
        bitmap = await decoder.GetSoftwareBitmapAsync(
            BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
    }

    Mat mat = null;
    using (var stream = new InMemoryRandomAccessStream())
    {
        var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.BmpEncoderId, stream);
        encoder.SetSoftwareBitmap(bitmap);
        await encoder.FlushAsync();

        mat = Mat.FromStream(stream.AsStreamForRead(), ImreadModes.Color);
    }

    var grayMat = new Mat();
    Cv2.CvtColor(mat, grayMat, ColorConversionCodes.BGR2GRAY);

    CvAruco.DetectMarkers(grayMat,
        CvAruco.GetPredefinedDictionary(PredefinedDictionaryName.Dict4X4_50),
        out Point2f[][] corners, out int[] ids, DetectorParameters.Create(), out Point2f[][] rejectedImgPoints);

    CvAruco.DrawDetectedMarkers(mat, corners, ids);

    using (var stream = new MemoryStream())
    {
        mat.WriteToStream(stream);
        using (var raStream = stream.AsRandomAccessStream())
        {
            var decoder = await BitmapDecoder.CreateAsync(raStream);
            bitmap = await decoder.GetSoftwareBitmapAsync(
                BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
        }
    }

    var imageSource = new SoftwareBitmapSource();
    await imageSource.SetBitmapAsync(bitmap);
    image.Source = imageSource;
}
OpenCvSharpは優れたライブラリで、.NETアプリケーションからOpenCVを使いやすくラップしてくれています。おかげでOpenCVでのARマーカー検出、描画は難しくありませんでした。 ところが、それ以前にUWPのSoftwareBitmapとOpenCVのMat型の相互の変換についてはなかなか消化することができず、あちこちのオンラインドキュメントやブログやフォーラムを何度も何度も巡回しつつ、試行錯誤しつつ行ったり来たりを繰り返してようやくそれらしい形にたどり着きました。 あまりそんなことばっかりやっているわけにはいきませんが、ある程度余裕を持て、藻掻いて、悩む時間も時には大事ですね。
これでもって改めて前回の記事をカメラで撮ってみる(横着)と、無事にARマーカーとそのID(3)が認識されていることが確認できました。

PHP7.4移行作業の苦労小話

はじめに

こんにちは、イメージ・マジックのもあいです。 今回は直近で作業をしていたPHP7.0からPHP7.4への移行作業について、手動での対応ではまった点についての備忘録です。

プロパティの型が指定できることによって・・・

PHP7.4の大きな違いの一つとしてはプロパティの型が明示できるようになったことがあると思いますが、これにより移行作業が思った以上に手間だったという印象がありました。自分が体験した内容を記していきます。 サンプルとして下記コードを用意しました。

<?php
declare(strict_types=1);

class Hoge {
        private $prop1;
        private string $prop2;
        public function getProp1() {
                return $this->prop1;
        }
        public function getProp2():string {
                return $this->prop2;
        }
}
$a = new Hoge();
var_dump($a->getProp1());
var_dump($a->getProp2());

サンプルとして用意したコードですが、このコードは16行目でエラーとなり実行が止まります。Hogeクラスのプロパティprop2ですが、string型を明示しているため、初期化しないでアクセスすると下記のようなエラーとなります。

PHP Fatal error:  Uncaught Error: Typed property Hoge::$prop2 must not be accessed before initialization in /home/moai/sample/test01.php:11

昔からPHPコードを書いていた人間としては、ここまでチェックされるとPHPの簡単なことが簡単なコードで実現できるという手軽さが無くなってきている印象を受けます。Javaのような言語に比べれば全然マシですけれども。それだけPHPが使われており単純なミスを抑止できるように型チェックをするようになっているとも言えます。 上記のエラーですが対応方法はいくつかあって、コンストラクタで初期化する、nullで初期化する、プロパティを参照するところで\Errorをcatchする、といったことができます。さすがに\Errorをcatchはやり過ぎな気もします。nullで初期化するのが妥当な気がします。

最後に

そのほかに、PHPが用意している文字列操作の関数引数にnullが入るとエラーになるという地味に面倒な物もありましたが、null合体演算子を入れれば比較的簡単に解決できますが、使用箇所が多いと地味に面倒でした。

行方不明の要素の捜索

株式会社イメージ・マジックの技術ブログ、先週の担当のsoenoです。
正月付近の投稿ということでそれっぽい何かを考えていたのですがもう2月だそうです。
流石に正月気分でもないので当初の予定はやめ、今回は行方不明の要素の捜索について書きます。

HTML上で消えた要素を探す。

html上で色々要素を置いていきふと気が付くと、html上は存在するあの要素がない!
そんなときのよくやる手口の紹介です。普通にやっている人もいるかと思いますが…

方法

開発者ツールなり、cssへのクラスの追加なりで以下の指定を追加。
.search{
    position: absolute;
    top: 0;
    left: 0;
    height: 100px;
    width: 100px;
    border: solid 5px red;
    background: blue;
    display: block;
  z-index: 9999999999;
  opacity: 1;
  color: green;
}
まず上の指定を捜索対象の要素に指定を足します。
クラスごと貼り付けるなり、ディベロッパーツールで追加するなりしてください。
この時打ち消されて効いていない箇所があった場合は衝突箇所を確認します。

次に表示が確認出来たら消えた原因を調べます。
(そのまま指定を足したままにするわけにはいきませんので)
付けた指定を一個づつ落とすなどし、怪しいところを絞り込みます。

最後に絞り込んだ箇所を問題のない指定に変えて終了です。
(疑似要素の場合はcontentsへの指定を追加するなど逐次書き換え。)

結論

今回この記事を書こうと思ったのは修正中のページの中で行方不明要素が出たからでした。
原因はdivの閉じタグが編集中に消えhtmlが崩れていたことでした。
まずhtmlをきれいに書き、行方不明の要素を出さないのが一番ということでしょうか。

RectorでSymfony4への変換

こんにちは、岡野です。

最近、Symfony2.8/PHP7.0で実装されたサービスをSymfony4.4/PHP7.4へバージョンアップしました。その際ソースコードの自動変換に使用したRectorというソフトウェアを紹介します。
詳しくは https://getrector.org/ を見ていただくとし、実際の変換結果を挙げていきます。    

変換結果

Symfony2.8 -> 2.8

元々Symfony2.8で実装していますが念のためRectorを実行します。

最初に設定ファイルを準備します。
$ vi rector.php
...
$parameters->set(Option::PATHS, [
    __DIR__.'/app',
    __DIR__.'/src',
]);
$parameters->set(Option::EXCLUDE_PATHS, [
    __DIR__.'/app/SymfonyRequirements.php',
    __DIR__.'/app/cache/',
    __DIR__.'/src/AppBundle/Tests/',
]);
$parameters->set(Option::PHP_VERSION_FEATURES, '7.0'); // 適宜変更
$parameters->set(
    Option::SYMFONY_CONTAINER_XML_PATH_PARAMETER,
    __DIR__.'/appDevDebugProjectContainer.xml'
);
そしてRectorを実行します。
$ vendor/bin/rector process --set symfony28
以下変換されました(以降、変換結果の一部を抜粋します)。
/**
 * @Route(defaults={"foo": ""}, ...)
 */
↓ ↓ ↓
/**
 * @Route(defaults={"foo"= ""}, ...)
 */
 

Symfony2.8 -> 3.0

$ vendor/bin/rector process --set symfony30
$form = $this->createForm(new Foo());
↓ ↓ ↓
$form = $this->createForm(\AppBundle\Form\Foo::class);
 

Symfony3.0 -> 3.4

(symfony31~symfony33は差分が発生しませんでした)
$ vendor/bin/rector process --set symfony34
/**
 * @Route(...)
 * @Method("POST")
 */
↓ ↓ ↓
/**
 * @Route(..., methods={"POST"})
 */
 

PHP7.0 -> 7.0

$ vendor/bin/rector process --set php70
isset($foo[$bar]) ? $foo[$bar]: 0
↓ ↓ ↓
$foo[$bar] ?? 0

rand();
↓ ↓ ↓
random_int(0, mt_getrandmax());
 

PHP7.0 -> 7.1

$ vendor/bin/rector process --set php71
list($a, $b) = $this->foo();
↓ ↓ ↓
[$a, $b] = $this->foo();

count($foo)
↓ ↓ ↓
is_array($foo) || $foo instanceof \Countable ? count($foo) : 0 // php73実行時に改善される
 

Symfony3.4 -> 4.2

(symfony40,symfony41は差分が発生しませんでした)
$ vendor/bin/rector process --set symfony42
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class FooController extends Controller
↓ ↓ ↓
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class FooController extends AbstractController
 

PHP7.1 -> 7.3

(php72は差分が発生しませんでした)
$ vendor/bin/rector process --set php73
is_array($foo) || $foo instanceof \Countable ? count($foo) : 0
↓ ↓ ↓
is_countable($foo) ? count($foo) : 0

json_encode($foo)
↓ ↓ ↓
json_encode($foo, JSON_THROW_ON_ERROR)

json_decode($foo)
↓ ↓ ↓
json_decode($foo, false, 512, JSON_THROW_ON_ERROR)
 

Symfony4.2 -> 4.3

$ vendor/bin/rector process --set symfony43
$event->getDispatcher()->dispatch(
    FOSUserEvents::SECURITY_IMPLICIT_LOGIN,
    new UserEvent($event->getUser(), $event->getRequest()));
↓ ↓ ↓
$event->getDispatcher()->dispatch(
    new UserEvent($event->getUser(), $event->getRequest()),
    FOSUserEvents::SECURITY_IMPLICIT_LOGIN);

use Symfony\Component\HttpKernel\Event\GetResponseEvent;
public function foo(GetResponseEvent $event)
↓ ↓ ↓
use Symfony\Component\HttpKernel\Event\RequestEvent;
public function foo(RequestEvent $event)
 

Symfony4.3 -> 4.4

$ vendor/bin/rector process --set symfony44
// Command class
public final function execute(InputInterface $input, OutputInterface $output)
↓ ↓ ↓
public final function execute(InputInterface $input, OutputInterface $output): int
 

PHP7.3 -> 7.4

$ vendor/bin/rector process --set php74
/**
 * @var integer
 * @ORM\Column(name="id", type="integer")
 * @ORM\Id
 * @ORM\GeneratedValue(strategy="AUTO")
 */
private $id;
↓ ↓ ↓
/**
 * @ORM\Column(name="id", type="integer")
 * @ORM\Id
 * @ORM\GeneratedValue(strategy="AUTO")
 */
private int $id;

/**
 * @var integer
 * @ORM\Column(name="foo", type="integer", nullable=true)
 */
private $foo;
↓ ↓ ↓
/**
 * @ORM\Column(name="foo", type="integer", nullable=true)
 */
private ?int $foo = null;

/** @var EntityManager */
private $em;
↓ ↓ ↓
private \Doctrine\ORM\EntityManager $em; // use形式への変更は手作業が必要

1024
↓ ↓ ↓
1_024 // この変換は微妙
 

Twig1.x -> 2.0

$ vendor/bin/rector process --set twig20
new \Twig_SimpleFilter(...)
↓ ↓ ↓
new \Twig_Filter(...)
 

Twig2.0 -> 2.4.0

$ vendor/bin/rector process --set twig240
new \Twig_Filter(...)
↓ ↓ ↓
new \Twig\TwigFilter(...)
 

まとめ

正規表現による置換よりもソフトウェアで一括変換した方が抜けも誤りも少ないでしょう。

だし誤変換もごく一部あります。未定義の配列へ要素追加しているコードが $foo = (array)$foo; $foo[] = $bar; と変換されるなどです(元々未定義なことが良くないのですが)。

って変換結果にざっと目を通すことは必要ですが、ほとんどはPHP/Symfony文法エラーのため容易に気付けます。変換後にPHPStormのInspect Code機能で確認することも有効でした。

当然
ながらRectorで変換できない点もありますので手動対応は必要です(src/AppBundle -> src/など)。

その他

­ 上記以外にもRectorのルールセットは多数ありますので興味ありましたら以下をご覧ください。
https://github.com/rectorphp/rector/tree/master/config/set

convertコマンド1回での画像処理【Imagemagick】

こんにちは。入社して約半年経ちました、ふくまです。
この時期は寒くてエアコン暖房必須です。少し前にSNSで「寝るとき暖房つけるか、つけないか」の質問をしたところ、30弱の票が集まり「暖房つけない」のほうが若干多かったです。 私は寒い時にエアコンも加湿器もガンガンに「つける」ので、個人的に衝撃の結果でした。
最近Imagemagickのコマンドと少しお友達になれたので、その内容について書いていきます。  

convertコマンドを1回で実行する理由

その方が処理が早いためです。 ですが、ネットで参考にさせていただいた記事だと、convertコマンドで画像を何回かに分けて生成し、最終的な画像を生成しているものが多かったです。

生成する画像

複数の画像処理を一括で行うため、今回はこれら2つの画像を使用することにします。(「いらすとや」さん、ありがとうございます。https://www.irasutoya.com/)  

すごい勢いで土下座をする人(syazai.png)  

とても怒っているカバ(kaba.png)

カンの良い方はもうお気づきかと思いますが、「怒ったカバに向けて全力で謝罪をする人」という構図の画像を生成します。
作業場所は上記2枚の画像を置いたディレクトリ内で、画像の生成先も同じ場所です。
コマンド作成手順は、1. kaba.pngを左右反転、2. syazai.pngを縮小、3. キャンバスの上に1,2を配置(コマンド1回で実行)、です。

convertコマンドを1回で実行する

1.kaba.pngを左右反転
convert kaba.png -flop output1.png
2.syazai.pngを縮小
convert syazai.png -resize 50% output2.png
3.キャンバスの上に1,2を配置(コマンド1回で実行)
convert \( -size 1255x705 xc:white -gravity west \( kaba.png -flop \) -composite -gravity southeast \( syazai.png -resize 50% \) -composite \) output.png

3の実行結果

今後見ることは無いであろう構図です。

コマンドの説明

3のコマンドを、外側から順を追って説明します。
convert \( A \) output.png
上記は、Aの処理の結果をoutput.pngに出力します。 次にAの中身を見ます。
-size 1255x705 xc:white -gravity west \( B \) -composite -gravity southeast \( C \) -composite
-size 1255×705 xc:whiteで、出力する画像サイズと背景白を定義しています。
そして-gravity west ( B ) -compositeで、キャンバスに左寄せでBの画像を出力することを指定しています(-compositeは複数画像を合成する際に使うオプションです)。
-gravity southeast ( C ) -compositeで、キャンバスに右下寄せでCの画像を出力します。
そして、Bは反転したカバ、Cは謝罪してる人の縮小した画像をそれぞれ返します。

最後に

今回はconvertコマンド1回で複数処理を実行する方法を紹介しました。
Imagemagickのコマンドは奥が深そうなので、今後も使い方を探索していきたいです。