🧻

Laravelでconstの管理が煩わしい時のテクニック

2023/12/20に公開

Social Databank Tech Blog Advent Calendar 2023の 20 日目です。
Laravelプロジェクトで、定数が多くて煩わしい!って思ったときにやったことが個人的に気に入っているので紹介します。

例えばこういうの

Formula.php
class Formula {
    public const OPERATOR_ADD = '+';
    public const OPERATOR_SUB = '-';
    public const OPERATOR_MUL = '*';
    public const OPERATOR_DIV = '/';
    public const OPERATOR_LEFT_PARENTHESES = '(';
    public const OPERATOR_RIGHT_PARENTHESES = ')';

    public const ALL_OPERATORS = [
        self::OPERATOR_ADD,
        self::OPERATOR_SUB,
        self::OPERATOR_MUL,
        self::OPERATOR_DIV,
        self::OPERATOR_LEFT_PARENTHESES,
        self::OPERATOR_RIGHT_PARENTHESES,
    ];

    public const CAlC_OPERATORS = [
        self::OPERATOR_ADD,
        self::OPERATOR_SUB,
        self::OPERATOR_MUL,
        self::OPERATOR_DIV,
    ];

    public const HIGH_PRIORITY_OPERATORS = [
        self::OPERATOR_MUL,
        self::OPERATOR_DIV,
    ];

    public const LOW_PRIORITY_OPERATORS = [
        self::OPERATOR_ADD,
        self::OPERATOR_SUB,
    ];

    // 数式文字列を逆ポーランド記法化する
    public function toReversePolish(string $formulaString) :array {
    }

    // 逆ポーランド記法化した数式の配列から計算結果を返す
    public function resolve(array $reversePolishArray) {
    }
}

数式の文字列を実際に計算するクラスがあったとします。
数式を逆ポーランド記法にして計算するわけですが、どういう演算子が使えるのかまとまっている必要があるわけですね。
その定数がconstで定義されています。

定数がたくさんありますねー、定数をまとめた定数もあります。煩わしいですね〜!
数式はこれ以上ロジックが増えないのでこれでいいかもしれませんが、
constがこれの10倍とかになったら嫌ですよね?煩わしいですよね?

煩わしいってなんだっけ

ゲシュタルト崩壊してきたので1回調べます
https://www.weblio.jp/content/煩わしい

心を悩ましてうるさい

しっくりきますね

個別のクラスで管理する

全部クラスにしてみよう

Operators/Add.php
//長くなるのでAddだけ
class Add implements Operator {
    public const SYMBOL = '+';
    public const PRIORITY = self::PRIORITY_LOW;

    public static function calc(int|float $a, int|float $b) :int|float {
        return $a + $b;
    }

    public static function canCalc() :bool {
        return self::calc(1, 1) !== false;
    }
}
Operators/Operator.php
interface Operator {
    public const PRIORITY_LOW = 1;
    public const PRIORITY_HIGH = 10;

    public static function calc(int|float $a, int|float $b) :int|float;
    public static function canCalc() :bool;
}
Formula.php
use Operators/Add;
use Operators/Sub;
use Operators/Mul;
use Operators/Div;
use Operators/LeftParentheses;
use Operators/RightParentheses;

class Formula {
    public const ALL_OPERATORS = [
        Add::SYMBOL,
        Sub::SYMBOL,
        Mul::SYMBOL,
        Div::SYMBOL,
        LeftParentheses::SYMBOL,
        RightParentheses::SYMBOL,
    ];

    public const CALC_OPERATORS = [
        Add::SYMBOL,
        Sub::SYMBOL,
        Mul::SYMBOL,
        Div::SYMBOL,
    ];

    public const HIGH_PRIORITY_OPERATORS = [
        Mul::SYMBOL,
        Div::SYMBOL,
    ];

    public const LOW_PRIORITY_OPERATORS = [
        Add::SYMBOL,
        Sub::SYMBOL,
    ];

    // 数式文字列を逆ポーランド記法化する
    public function toReversePolish(string $formulaString) :array {
    }

    // 逆ポーランド記法化した数式の配列から計算結果を返す
    public function resolve(array $reversePolishArray) {
    }
}

気になるところがいくつかありますね!

  • 何か追加するたびインポートとAll_OPERATORSを更新しないといけない
  • せっかく優先度とか計算可能な演算子なのかとかクラスで管理しているのに使ってない

LaravelのCommandだと自動でインポートしてphp artisanほにゃららで呼び出せたりしますよね
ってことで、そのコードを持ってきて解決します!

(あとは「せっかくクラス化したのにSYMBOLで扱うの?」とか「constの命名にSYMBOL入れるべきじゃない?」とかあるけど、今回の話では別にどっちでもいいので一旦ヨシッとさせてください)

Laravelで使われているコードを参考にTraitを作る

https://github.com/laravel/framework/blob/56cf2018490ea0193f50bf2bff59654a46cf52d8/src/Illuminate/Foundation/Console/Kernel.php#L343-L352

これをこうじゃ

ImportableClasses.php
use Illuminate\Support\Facades\App;
use Illuminate\Support\Str;
use Symfony\Component\Finder\Finder;

trait ImportableClasses {
    protected array $importedClasses;

    protected function importClasses() :void
    {
        $namespace = App::getNamespace();
        foreach ((new Finder)->in(self::getPath())->files() as $file) {
            $fileName = Str::after(
                $file->getRealPath(),
                realpath(app_path()) . DIRECTORY_SEPARATOR
            );
            $className = str_replace(['/', '.php'], ['\\', ''], $fileName);
            $class = $namespace . $className;
            if (!(new \ReflectionClass($class))->isInstantiable()) {
                // interfaceとかabstractとかtraitはスルー
                continue;
            }
            if (!self::canImport($class)) {
                // 取りたいファイルじゃなかったらスルー
                continue;
            }

            $this->importedClasses[] = $class;
        }
    }

    abstract protected function getPath() :string;
    abstract protected function canImport(string $class) :bool;
}

使ってみる

Formula.php
use Operators\Operator;

class Formula {
    use ImportableClasses;

    public function __construct()
    {
        $this->importClasses();
    }

    protected function getPath() :string {
        return __DIR__ . '/Operators';
    }

    protected function canImport(string $class) :bool {
        // 一応Operatorを継承しているものだけに絞る
        return is_subclass_of($class, Operator::class);
    }

    public function getAllOperators() :array {
        return $this->toSymbolsFrom($this->importedClasses);
    }

    public function getCalcOperators() :array {
        return $this->toSymbolsFrom(array_filter(
            $this->importedClasses,
            fn($class) => $class::canCalc()
        ));
    }

    public function getOperatorsByPriority(int $priority) :array {
        return $this->toSymbolsFrom(array_filter(
            $this->importedClasses,
            fn($class) => $class::PRIORITY === $priority
        ));
    }

    public function toSymbolsFrom($classes) :array {
        return array_map(fn($class) => $class::SYMBOL, $classes);
    }

    // 数式文字列を逆ポーランド記法化する
    public function toReversePolish(string $formulaString) :array {
    ...
    }

    // 逆ポーランド記法化した数式の配列から計算結果を返す
    public function resolve(array $reversePolishArray) {
    ...
    }
}

最終的にconstがメソッドになっちゃいます
必然的に変数を持つのでFacade化も検討しましょう

いかがでしたでしょうか

だいぶスッキリしましたね(行数変わらんけど)
新しいOperatorを追加してもFormulaクラスをいじる必要が一切ありません
いじるとしてもロジックあたり(toReversePolishとかresolveとか)ですかね

デメリットとしては、

  • ぱっと見でOperatorがどれだけあるのかわからない(ディレクトリ見ればいい)
  • 優先度とか計算の可否で絞り込まれたOperatorもどれだけあるのかわからない(tinkerで実行するか、ディレクトリ分けで対処することになりそう)

なのでconstがたくさんあって読みづらく、管理が「煩わしい」と思ったら試してみてください

ソーシャルデータバンク テックブログ

Discussion