Goでis-a関係を作る

Goには継承はなく、実装の再利用は構造体の埋め込みで行う。じっさいは委譲だが、見た目はオブジェクト指向言語で継承した場合のように使える。

package main

type Super struct {}
func (super Super) SuperMethod() {}

type Sub struct {Super}

func main() {
    sub := Sub{Super{}}
    sub.SuperMethod() // Subから直接Superのメソッドを実行できる
}

ただしこれだと当然is-a関係にはならないため、以下のようにSuperを要求する関数でSubは使えない。

package main

func RequiresSuper(super Super) Super {
    return super
}

type Super struct {}
func (super Super) SuperMethod() {}

type Sub struct {Super}

func main() {
    sub := Sub{Super{}}
    sub.SuperMethod()

    super := RequiresSuper(sub) // コンパイルエラーになる
    super.SuperMethod()
}

sub.Superを渡すことで済む場合もあろうが、当然それはSuper型の変数となってしまうので、今度はSubのメソッドを実行できない。

package main

func RequiresSuper(super Super) Super {
    return super
}

type Super struct {}
func (super Super) SuperMethod() {}

type Sub struct {Super}

func main() {
    sub := Sub{Super{}}
    sub.SuperMethod()

    super := RequiresSuper(sub.Super) // ここを変更
    super.SuperMethod()
    super.SubMethod() // コンパイルエラー
}

どうするか。インターフェイスを使う。SuperSuperInterfaceを満たすようにし、Superを直接でなく、SuperInterfaceを要求するようにする。

package main

func RequiresSuper(super SuperInterface) SuperInterface {
    return super
}

type SuperInterface interface { SuperMethod() }

type Super struct {}
func (super Super) SuperMethod() {}

type Sub struct {Super}
func (sub Sub) SubMethod() {}

func main() {
    sub := Sub{Super{}}
    sub.SuperMethod()
    sub.SubMethod()

    super := RequiresSuper(sub)
    super.SuperMethod()
    super.(Sub).SubMethod() // とはいえ、SuperInterfaceはSubMethod()を当然持っていないので、ここではどうしても型表明が必要になる
}

ここまで辿り着くのにかなりかかった。

PHPで例外を出力するときにわざわざgetMessage()を使う必要はない

たまに見かけるので。

こんな感じにすれば__toString()が自動的に呼ばれる。エラーメッセージとスタックトレースの両方がついたいい感じの出力になる。

<?php

function a() {
    throw new Exception("test");
}

try {
    a();
}
catch (Exception $e) {
    echo $e;
}
exception 'Exception' with message 'test' in /tmp/vw50zKH/28:4
Stack trace:
#0 /tmp/vw50zKH/28(8): a()
#1 {main}

文字列コンテキストになっているか不安な場合は、"$e"とすれば安心。

getMessage()スタックトレースがないし、getTraceAsString()は逆にメッセージがない。そもそもどちらもファイル名・行数に関する表示はない。加工してrethrowするような場合以外は、__toString()相当がおすすめ。

Nginxで権限があるはずなのにポートをバインドできない

ちょっと必要があって、Nginxを8000番で動かそうとしたらなぜかエラーが出た。1023までのポートならともかく8000だし、そもそもroot権限で動かしてるのに。

nginx: [emerg] bind() to 0.0.0.0:8000 failed (13: Permission denied)

Permission deniedと来たらseLinuxかなとaudit.logを見てみる。

type=AVC msg=audit(1564721070.008:4043): avc:  denied  { name_bind } for  pid=26994 comm="nginx" src=8000 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:soundd_port_t:s0 tclass=tcp_socket permissive=0

やっぱり。semanageというコマンドで許可できるっぽいが入ってない。yum providesして調べる。policycoreutils-pythonというパッケージに含まれてるとのこと。

sudo yum install policycoreutils-python

するとインストール済みと出る。なぜ。

sudo yum reinstall policycoreutils-python

で入ったっぽい。で、8000ポートをHTTP扱いに。

sudo semanage port -a -t http_port_t -p tcp 8000

すると、

ValueError: Port tcp/8000 already defined

ほかで使われているようだ。

sudo semanage port -l | grep 8000
# soundd_port_t                  tcp      8000, 9433, 16001

-mだと追加できるぽい。

sudo semanage port -m -t http_port_t -p tcp 8000

WindowsでMercurialが死んでた件

5.0.2で以下のようなエラーが出た。Python 3が別途入ってたからそのせいかと思ってPATHいじったりアンインストールしてみたりしたが駄目。

仕方ないので一応安全そうなところから4.9.1をダウンロードして入れてみたら動いたっぽい。

ちなみにChocolateyで定期的に自動アップデートしてる上、Mercurial自体も手動ではもう使っていないので気付くのがだいぶ遅れた。気をつけないと。

$ hg
Traceback (most recent call last):[2019-07-29 19:58]
  File "hg", line 43, in <module>
  File "hgdemandimport\demandimportpy2.pyc", line 150, in __getattr__
  File "hgdemandimport\demandimportpy2.pyc", line 94, in _load
  File "hgdemandimport\demandimportpy2.pyc", line 43, in _hgextimport
  File "mercurial\dispatch.pyc", line 22, in <module>
  File "hgdemandimport\demandimportpy2.pyc", line 248, in _demandimport
  File "hgdemandimport\demandimportpy2.pyc", line 43, in _hgextimport
  File "mercurial\i18n.pyc", line 28, in <module>
  File "hgdemandimport\demandimportpy2.pyc", line 150, in __getattr__
  File "hgdemandimport\demandimportpy2.pyc", line 94, in _load
  File "hgdemandimport\demandimportpy2.pyc", line 43, in _hgextimport
  File "mercurial\encoding.pyc", line 24, in <module>
  File "mercurial\policy.pyc", line 101, in importmod
  File "mercurial\policy.pyc", line 63, in _importfrom
  File "hgdemandimport\demandimportpy2.pyc", line 164, in __doc__
  File "hgdemandimport\demandimportpy2.pyc", line 94, in _load
  File "hgdemandimport\demandimportpy2.pyc", line 43, in _hgextimport
  File "mercurial\cext\parsers.pyc", line 12, in <module>
  File "mercurial\cext\parsers.pyc", line 10, in __load
ImportError: DLL load failed: %1 は有効な Win32 アプリケーションではありません。

しかしググってももうMercurialの情報がろくに出てこなくてね、つらい……。

Laravelでドキュメントルートに静的ファイルを混在させる方法

作ったシステムに後から静的ページを追加したい、みたいな要望にどう対応すればいいだろうか。「有料です」で済ませたいところだが、なかなかそうも行かない場合も多い。

古のCGI/PHP環境でやってきたクライアントにとっては、既存のファイルと無関係なところにファイル/ディレクトリを追加することができないのはなぜ、となってしまうのもまあ仕方ないのかもしれない。

そういうときどうするか。

LaravelのデフォルトのApache/Nginxの設定では、ファイルが存在する場合はそちらを読み込むようになっているので、単純にpublicディレクトリにファイルを置けばそちらを優先してくれるが、publicディレクトリは普通Git管理下にあるので、勝手にいじられたくない。1

解決法は、publicディレクトリをドキュメントルートのサブディレクトリなどに置いて触らないよう伝えた上で、ほかは勝手にしてください、とすることだろうか。

ということで、ドキュメントルートにpublic/.htaccessベースの.htaccessを置いて、URL上ではルートで動かしながら、じっさいのファイルはドキュメントルートのサブディレクトリに置くようにしてみる。

以下ではサブディレクトリ名はpublicとする。

<IfModule mod_rewrite.c>
    <IfModule mod_negotiation.c>
        Options -MultiViews -Indexes
    </IfModule>

    RewriteEngine On

    # Handle Authorization Header
    RewriteCond %{HTTP:Authorization} .
    RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

    # Redirect Trailing Slashes If Not A Folder...
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_URI} (.+)/$
    RewriteRule ^ %1 [L,R=301]

    # ここから追加 {
    RewriteCond %{REQUEST_URI} ^/$
    RewriteRule ^ public/index.php [L]

    RewriteCond %{REQUEST_FILENAME} !-f # これがあれば、ドキュメントルート直下の同名ファイルを優先できるがなくてもいい。
    RewriteCond %{DOCUMENT_ROOT}/public%{REQUEST_URI} -f
    RewriteRule (.*) public/$1
    # } ここまで追加

    # Handle Front Controller...
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^ public/index.php [L] # ここも変更
</IfModule>

なおこれだけでは、https://example.com/public/への直アクセスが防げない。いい方法が思い付かなかった。思い付いた人いたら教えてください。


  1. デプロイ方法にもよる。リンクでなく一方向のコピーであれば、上書きさえしなければ問題ない。

LaravelでAuth系のコントローラが最初から存在するのはなぜか

ご存知の通りLaravelではartisan make:authすることでいくつかのファイルが生成・変更され、デフォルトの認証関係の機能を有効にできる。

だが、じつはこれを実行しなくとも、最初からApp\Http\Controllers\Auth\LoginControllerをはじめとする各種コントローラが存在している。

GitHubで履歴を辿って調べてみた。確証はないがおそらく、単純な歴史的経緯が理由のようだ。


認証用のコントローラが laravel/laravel (Laravelのフレームワークのコア部分を除いたリポジトリ。いくつかのサンプル的なコードが含まれる)上で実装されたのは5.0のころ1のようだ。

この時点ではmake:authはなかった。こちらは5.2のころ2に実装されている。

もう一つ興味深いのが、このとき実装されたmake:authが一度削除されている3ということだ。各種ファイルがlaravel/laravelにすでに含まれているから不要という理由で削除されている。このときは5.8現在とは違い、make:authが認証用のコントローラも生成していたようで、そのせいで重複と考えられたのだろう。

その後5.2のうち4に再度実装され、今度はlaravel/laravel側で存在するコントローラは生成されない形で実装された。

ということで歴史だけ見ると、先にlaravel/laravel内のファイルとして実装してしまったので、というだけでほかの理由などはなさそうだ。

現状はいまいちなので、laravel/laravelから認証用コントローラを削除し、make:authで生成できるようになって欲しいが、リポジトリが違うことからいろいろ面倒そうではある。


余談なのだが、make:authの実体となるファイルは一度削除される前と後で名前が変わり、さらにその後再度名前が戻される形で変更されており、そのせいで履歴を辿るのがちょっとややこしい。5

ChocolateyでStrawberry Perlが更新できない

chocolatey upgrade strawberryperlすると、

WARNING: Generic MSI Error. This is a local environment error, not an issue with a package or the MSI itself - it could mean a pending reboot is necessary prior to install or something else (like the same version is already installed). Please see MSI log if available. If not, try again adding '--install-arguments="'/l*v c:\StrawberryPerl_msi_install.log'"'. Then search the MSI Log for "Return Value 3" and look above that for the error.
ERROR: Running ["C:\Windows\System32\msiexec.exe" /i "C:\Users\********\AppData\Local\Temp\chocolatey\StrawberryPerl\5.30.0.1\strawberry-perl-5.30.0.1-64bit.msi" /qn /norestart /l*v c:\StrawberryPerl_msi_install.log] was not successful. Exit code was '1603'. Exit code indicates the following: Generic MSI Error. This is a local environment error, not an issue with a package or the MSI itself - it could mean a pending reboot is necessary prior to install or something else (like the same version is already installed). Please see MSI log if available. If not, try again adding '--install-arguments="'/l*v c:\StrawberryPerl_msi_install.log'"'. Then search the MSI Log for "Return Value 3" and look above that for the error..
The upgrade of strawberryperl was NOT successful.
Error while running 'C:\ProgramData\chocolatey\lib\StrawberryPerl\tools\chocolateyInstall.ps1'.
 See log for details.
 Unsuccessful operation for strawberryperl.

とか出る。

書いてある通り、

--install-arguments="'/l*v c:\StrawberryPerl_msi_install.log'"

を追加して再実行してみる。出力されたログにこんなことが書いてあった。

MSI (s) (E8:F8) [09:12:05:418]: Doing action: CA_UninstallOldVersion
Action ended 9:12:05: FindRelatedProducts. Return value 1.
Action start 9:12:05: CA_UninstallOldVersion.
MSI (s) (E8:F8) [09:12:05:418]: Product: Strawberry Perl (64-bit) -- Already installed version of Strawberry Perl (64-bit) cannot be upgraded; it has to be uninstalled first.

Already installed version of Strawberry Perl (64-bit) cannot be upgraded; it has to be uninstalled first.
Action ended 9:12:05: CA_UninstallOldVersion. Return value 3.
Action ended 9:12:05: INSTALL. Return value 3.

先にアンインストールしろってことかな?

C:\Users\********\AppData\Local\Temp\chocolatey\StrawberryPerl\5.30.0.1\strawberry-perl-5.30.0.1-64bit.msi

にあるのを直接叩いてみると、GUIで同じことを言われた。

つまりChocolateyのStrawberry Perlのパッケージでは、前のバージョンがうまくアンインストールできない(ことがある?)らしい。

もうちょっと調べたい気もしたが、今回はここまで。とりあえず手動でアンインストール後、再度、

choco upgrade strawberryperl

した。