[laravel8.x] hyn/multi-tenant その2

fqdnとは”Fully Qualified Domain Name”の略
トップレベルドメイン(TLD)までのすべてのラベルを含むドメイン名

routes/tenants.php

use Illuminate\Support\Facades\Route;

Route::get('/', function(){
	return view('tenants.home');
});

resources/views/tenants/home.php

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Document</title>
</head>
<body>
	<h1>Welcom!</h1>
	<p>This will be your <b>dashboard</b> for every tenant in your system.</p>
</body>
</html>

config/tenancy.php
L make sure that path would be right.

    'routes' => [
        'path' => base_path('routes/tenants.php'),
        'replace-global' => false,
    ],

### creating first tenant
$ php artisan make:command tenant/create
app/Console/Commands/tenant/create.php

namespace App\Console\Commands\tenant;

use Illuminate\Console\Command;

use Illuminate\Support\Str;

use Hyn\Tenancy\Models\Hostname;
use Hyn\Tenancy\Models\Website;
use Hyn\Tenancy\Repositories\HostnameRepository;
use Hyn\Tenancy\Repositories\WebsiteRepository;

class create extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    // protected $signature = 'command:name';
    protected $signature = 'tenant:create {fqdn}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Given a unique tenant name, creates a new tenant in the sytem';

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

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

        $fqdn = sprintf('$s.$s', $this->argument('fqdn'), env('APP_DOMAIN'));

        $website = new Website;
        $website->uuid = env('TENANT_WEBSITE_PREFIX') . Str::random(6);
        app(WebsiteRepository::class)->create($website);

        $hostname = new Hostname;
        $hostname->fqdn = $fqdn;
        $hostname = app(HostnameRepository::class)->create($hostname);
        app(HostnameRepository::class)->attach($hostname, $website);
        // return 0;
    }
}

.env

TENANT_WEBSITE_PREFIX=tenancy_demo_

php artisan tenant:create demo

mysql> show tables;
+————————+
| Tables_in_tenancy_demo |
+————————+
| failed_jobs |
| hostnames |
| migrations |
| password_resets |
| users |
| websites |
+————————+
6 rows in set (0.31 sec)

mysql> select * from websites;
+—-+———————+———————+———————+————+——————————–+
| id | uuid | created_at | updated_at | deleted_at | managed_by_database_connection |
+—-+———————+———————+———————+————+——————————–+
| 1 | tenancy_demo_NcmmUL | 2021-08-08 04:39:43 | 2021-08-08 04:39:43 | NULL | NULL |
| 2 | tenancy_demo_IbSPrJ | 2021-08-08 04:47:13 | 2021-08-08 04:47:13 | NULL | NULL |
+—-+———————+———————+———————+————+——————————–+
2 rows in set (0.37 sec)

conf.dの名前解決がうまくいっていないような印象。
あれ? マルチテナントの場合、サブドメインの設定ってするんだっけ?

apacheのhttp.confの設定

さくらVPSでtenancyをテストするために、お名前.comで取得したドメインを設定していきます。

$ sudo vi /etc/httpd/conf.d/custom.conf

DocumentRoot "/var/www/html/tenancy-demo/public"
# .htaccess 有効化
<Directory /var/www/html/tenancy-demo/public>
Options Indexes FollowSymLinks MultiViews
AllowOverride All
Order allow,deny
allow from all
Require all granted
</Directory>

$ service httpd restart

[CentOS8.3] php8.0のインストール

centOS8.3にphp8.0をインストールします。

# cat /etc/redhat-release
CentOS Linux release 8.3.2011

# dnf install https://rpms.remirepo.net/enterprise/remi-release-8.rpm
# dnf module list php
CentOS Linux 8 – AppStream
Name Stream Profiles Summary
php 7.2 [d] common [d], devel, minimal PHP scripting language
php 7.3 common [d], devel, minimal PHP scripting language
php 7.4 common [d], devel, minimal PHP scripting language

Remi’s Modular repository for Enterprise Linux 8 – x86_64
Name Stream Profiles Summary
php remi-7.2 common [d], devel, minimal PHP scripting language
php remi-7.3 common [d], devel, minimal PHP scripting language
php remi-7.4 common [d], devel, minimal PHP scripting language
php remi-8.0 common [d], devel, minimal PHP scripting language

# dnf install php80
# dnf install php80-php-cli php80-php-pdo php80-php-fpm php80-php-json php80-php-mysqlnd php80-php-mbstring php80-php-xml
# php80 -v
# sudo alternatives –install /usr/bin/php php /usr/bin/php80 1
# php -v
PHP 8.0.9 (cli) (built: Jul 29 2021 12:53:58) ( NTS gcc x86_64 )
Copyright (c) The PHP Group
Zend Engine v4.0.9, Copyright (c) Zend Technologies

OK^^

[Laravel 8.x] Multi-Tenant implementation with hyn/multi-tenant

### プロジェクト作成
$ composer create-project –prefer-dist laravel/laravel tenancy-demo
$ cd tenancy-demo
$ composer require hyn/multi-tenant

### mysql driverインストール
$ composer require tenancy/db-driver-mysql
Problem 1
– tenancy/db-driver-mysql[v1.3.0, …, 1.x-dev] require doctrine/dbal ^2.9 -> found doctrine/dbal[v2.9.0, …, 2.13.x-dev] but the package is fixed to 3.1.1 (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.
– Root composer.json requires tenancy/db-driver-mysql ^1.3 -> satisfiable by tenancy/db-driver-mysql[v1.3.0, 1.x-dev].

doctrine/dbalを2系でインストールする必要がありそう。

$ composer require “doctrine/dbal:2.*”
$ composer require tenancy/db-driver-mysql
今度は上手くいきました。

config/app.php

    'providers' => [
        // 省略
        Hyn\Tenancy\Providers\TenancyProvider::class,
        Hyn\Tenancy\Providers\WebserverProvider::class,

    ],

file update
$ php artisan vendor:publish –tag tenancy

config/database.php
L connectionでmysqlをコピペしてsystemを追加します。

    'connections' => [

        'system' => [
            'driver' => 'mysql',
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'tenancy'),
            'username' => env('DB_USERNAME', 'hoge'),
            'password' => env('DB_PASSWORD', 'fuga'),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'prefix_indexes' => true,
            'strict' => true,
            'engine' => null,
            'options' => extension_loaded('pdo_mysql') ? array_filter([
                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
            ]) : [],
        ],
    ],

.env

TENANCY_HOST=127.0.0.1
TENANCY_PORT=3306
TENANCY_DATABASE=tenancy_demo
TENANCY_USERNAME=hoge
TENANCY_PASSWORD=fuga

mysql> create database tenancy_demo;
Query OK, 1 row affected (0.01 sec)

$ php artisan migrate –database=system
Migrating: 2017_01_01_000003_tenancy_websites
Migrated: 2017_01_01_000003_tenancy_websites (16.88ms)
Migrating: 2017_01_01_000005_tenancy_hostnames
Migrated: 2017_01_01_000005_tenancy_hostnames (44.02ms)
Migrating: 2018_04_06_000001_tenancy_websites_needs_db_host
Migrated: 2018_04_06_000001_tenancy_websites_needs_db_host (12.21ms)

### Trying your installation
multi tenantのテストを行うには、apacheかlaravel Valetでテストする必要がある
localhost:[any_port] では動かない
tenant.system.extension でないとダメ

なんだとおおおおおおおおおおおおお
どうりでphp artisan serveでvagrant環境で動かないと思った。
うーん、困った。さくらVPSでテストするか。
というか、Laravel Valetってなんだ??

### laravel valetとは?
macOSミニマリスト向けのLaravel開発環境
https://readouble.com/laravel/8.x/ja/valet.html

うーむ、結局apacheでやらないと駄目そうだな。。

[Laravel 8.45.1] マルチテナントアーキテクチャ

Laravelでのマルチテナントは、tenancy/tenancyを使用する。
tenancy/tenancyと、ベースの機能のみで必要な分は後から追加するtenancy/frameworkがある。
tenancy/framework推奨

$ composer require tenancy/framework
$ php artisan make:model Organization -m

create_organizations_table.php

public function up()
    {
        Schema::create('organizations', function (Blueprint $table) {
            $table->id();
            $table->string('name')->unique();
            $table->timestamps();
        });
    }

$ php artisan migrate

Organization.php

use Illuminate\Http\Request;
use Tenancy\Identification\Contracts\Tenant;

class Organization extends Model implements Tenant
{
    use HasFactory;

    protected $fillable = [
    	'name',
    	'subdomain',
    ];

    protected $dispatchesEvents = [
    	'created' => \Tenancy\Tenant\Events\Created::class,
    	'updated' => \Tenancy\Tenant\Events\Updated::class,
    	'deleted' => \Tenancy\Tenant\Events\Deleted::class,
    ];

    public function getTenantKeyName(): string {
    	return 'name';
    }

    public function getTenantKey(){
    	return $this->name;
    }

    public function getTenantIdentifier(): string {
    	return get_class($this);
    }
}

Tenancy\Identification\Concerns\AllowsTenantIdentification を使うだけでもできる
-テナントが追加されたら、テナント用のデータベースを作成してマイグレーション
-テナントが更新されたらマイグレーション実行
-テナントが削除されたらデータベース削除
-テナントを識別するため、Resolverに登録

app/Providers/AppServiceProvider

use Tenancy\Identification\Contracts\ResolvesTenants;

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        //
        $this->app->resolving(ResolvesTenants::class, function(ResolvesTenants $resolver){
            $resolver->addModel(Organization::class);
            return resolver;
        });
    }
}

MySQLのデータベースドライバをインストール
$ composer require tenancy/db-driver-mysql

affects connections : テナントのDBにテナントのDBにアクセスできるようにする
$ composer require tenancy/affects-connections

tenancyの拡張パッケージは Event/Listner で構成
$ php artisan make:listener ResolveTenantConnection
/listeners/ResolveTenantConnection.php

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Tenancy\Identification\Contracts\Tenant;
use Tenancy\Affects\Connections\Contracts\ProvidesConfiguration;

class ResolveTenantConnection
{
    public function handle(Resolving $event)
    {
        //
        return $this;
    }

    public function configure(Tenant $tenant): array {
        $config = [];

        event(new Configuring($tenant, $config, $this));

        return $config;
    }
}

/app/Providers/EventServiceProvider.php

use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;

class EventServiceProvider extends ServiceProvider
{
    protected $listen = [

        \Tenancy\Affects\Connections\Events\Resolving::class => [
            App\Listeners\ResolveTenantConnection\ResolveTenantConnection::class,
        ],
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
    ];
}

$ composer require tenancy/hooks-migration
$ php artisan make:listener ConfigureTenantMigrations

use Tenancy\Hooks\Migration\Events\ConfigureMigrations;
use Tenancy\Tenant\Events\Deleted;

class ConfigureTenantMigrations
{
    public function handle(ConfigureMigrations $event)
    {
        //
        if($event->event->tenant){
            if($event->event instanceof Deleted){
                $event->disable();
            } else {
                $event->path(database_path('tenant/migrations'));
            }
        }
    }
}

EventServiceProvider.php

use Illuminate\Support\Facades\Event;
use App\Listeners\ConfigureTenantMigrations;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
        $this->app->resolving(ResolvesTenants::class, function(ResolvesTenants $resolver){
            $resolver->addModel(Organization::class);
            return resolver;
        });
    }

    protected $listen = [
        \Tenancy\Hooks\Migration\Events\ConfigureMigrations::class => [
            ConfigureTenantMigrations::class,
        ],
    ];
}

$ php artisan make:listener ConfigureTenantSeeds

use Database\Tenant\Seeders\DatabaseSeeder;
use Tenancy\Hooks\Migration\Events\ConfigureSeeds;
use Tenancy\Tenant\Events\Deleted;
use Tenancy\Tenant\Events\Updated;

class ConfigureTenantSeeds
{
    public function handle(ConfigureSeeds $event)
    {
        //
        if($event->event->tenant){
            if($event->event instanceof Deleted || $event->event instanceof Updated){
                $event->disable();
            } else {
                $event->seed(DatabaseSeeder::class);
            }
        }
    }
}

EventServiceProvider.php

    protected $listen = [

        \Tenancy\Affects\Connections\Events\Resolving::class => [
            App\Listeners\ResolveTenantConnection\ResolveTenantConnection::class,
        ],
        \Tenancy\Hooks\Migration\Events\ConfigureMigrations::class => [
            ConfigureTenantMigrations::class,
        ],
        \Tenancy\Hooks\Migration\Events\ConfigureSeeds::class => [
            ConfigureTenantSeed::class
        ],
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
    ];

.env.testing

TENANT_SLUG=test

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=test_central
DB_USERNAME=root
DB_PASSWORD=password

あれ? これでどうやって実装するのかようわからんな。。

エンジニアがベンチャーキャピタルと面談(資金調達)する時のコツ

エンジニアであれば、プロダクトを開発してベンチャーキャピタルに資金調達を依頼する機会が何度かあるであろう。
今日私はVCと面談した。その経験を元に、大事なことと感じた内容を整理したい。

■1.事業の拡大
これが全てだと断言して良い。ベンチャーキャピタルが興味を持っているのは「どうやって事業を拡大するか」の一点に尽きる。もちろんIPOを見据えての事だが、Buyoutでも数十億以上で売れないと意味がないらしい。
つまり、スケールしなければ、全く興味がなく、「equityで調達する意味ない」と言われる。
よって、どうやって事業を拡大するかを説得できるように準備して面談に望む事。

■2.課題・悩み事
どういうことに悩んでいるか、面談で言ってみると良い。それに対して、ベンチャーキャピタリストの見解を答えてくれる。

■3.技術的なアドバンテッジについて
VCはコンサルに近い。相手によるかと思うが、どういう技術を使っているかなどは、聞かれたら答える程度で良い。
企画や営業と話す時のように、噛み砕いで話す姿勢を見せた方が良いように思った。
それよりもビジネスとして競合よりもどう優れているかを説明しなければいけない。

■4.質問の準備
10個くらい質問を準備して面談に臨んだが、全く意味がなかった。
就活の面談ではないので、「相手に興味を示す」というのは全然求められていないようだった。
(投資で重視されていること、サポート体制、何故VCをやっているのかなど色々聞こうと思っていたが、それはナンセンスな質問だと悟った)

■5.実績のアピールが重要
サービスに関する実績をアピールすることが大事。
「どういう実績があるからこのサービスを作っている」ということを上手く説明するのが大事だ。
VCは、何故そのサービスをやっているかについては興味を持っている。

ということで、私は面談は撃沈したのだが、参考にして欲しい。
ベンチャーキャピタルの面談では、開発プロジェクトの面談などと違って、ゼロから1まで全て開発したとか、どの機能の開発に工夫しているなんてことは一切聞かれない。

——-
追加情報1
■6. ストーリーが大事
営業戦略、売上をどう立てていくか

■7. 売上10億立てないと駄目
上場基準として10億円ないと上場できない

■8. バリエーション
リード投資もしくは事業会社にバリエーションを作ってもらう

——-
追加情報2
■9. 組織体制
強いエンジニア、営業スタッフを揃えているか重視している

■10. ユーザヒヤリング
ニーズをきちんと聞くこと
ヒヤリングの仕組み作りを検討する

■11. 組織的な強みを作ること
競合にはない強みを持つこと

■12. 事業計画の立て方
事業計画の作成能力は大事
初年度などは赤字の方が見栄えが良い
売上が取れないことが最も多く致命的な問題

■13. リプレイス
システムのリプレイスの売り方を考える

[Python3 音声認識] juliasを使ってみる

### juliusとは
– 京大や名古屋工業大が開発しているオープンソース音声認識ライブラリ
– C言語で書かれている
– 独自の辞書モデルを定義することが可能
– 単体では動作せず、言語認識をするモデルを読み込んで動かす
– Juliux GitHub: https://github.com/julius-speech/julius

### julius install
$ sudo yum install build-essential zlib1g-dev libsdl2-dev libasound2-dev
$ git clone https://github.com/julius-speech/julius.git
$ cd julius
$ ./configure –enable-words-int
$ make -j4
$ ls -l julius/julius
-rwxrwxr-x 1 vagrant vagrant 700208 Jul 31 16:39 julius/julius

### julius model
https://sourceforge.net/projects/juliusmodels/
「ENVR-v5.4.Dnn.Bin.zip」をDownloadします。
$ unzip ENVR-v5.4.Dnn.Bin.zip
$ cd ENVR-v5.4.Dnn.Bin
$ ls
ENVR-v5.3.am ENVR-v5.3.layer5_weight.npy ENVR-v5.3.prior
ENVR-v5.3.dct ENVR-v5.3.layer6_bias.npy README.md
ENVR-v5.3.layer2_bias.npy ENVR-v5.3.layer6_weight.npy dnn.jconf
ENVR-v5.3.layer2_weight.npy ENVR-v5.3.layerout_bias.npy julius-dnn-output.txt
ENVR-v5.3.layer3_bias.npy ENVR-v5.3.layerout_weight.npy julius-dnn.exe
ENVR-v5.3.layer3_weight.npy ENVR-v5.3.lm julius.jconf
ENVR-v5.3.layer4_bias.npy ENVR-v5.3.mfc mozilla.wav
ENVR-v5.3.layer4_weight.npy ENVR-v5.3.norm test.dbl
ENVR-v5.3.layer5_bias.npy ENVR-v5.3.phn wav_config

$ sudo vi dnn.jconf

feature_options -htkconf wav_config -cvn -cmnload ENVR-v5.3.norm -cvnstatic
// 省略
state_prior_log10nize false // 追加

### Recognize Audio File
$ ../julius/julius/julius -C julius.jconf -dnnconf dnn.jconf
————-pass1_best: the shower
pass1_best_wordseq: the shower
pass1_best_phonemeseq: sil | dh iy | sh aw ax r
pass1_best_score: 130.578949
### Recognition: 2nd pass (RL heuristic best-first)
STAT: 00 _default: 21497 generated, 4114 pushed, 332 nodes popped in 109
ALIGN: === word alignment begin ===
sentence1: the shower
wseq1: the shower
phseq1: sil | dh iy | sh aw ax r | sil
cmscore1: 0.552 0.698 0.453 1.000
score1: 88.964218
=== begin forced alignment ===
— word alignment —
id: from to n_score unit
—————————————-
[ 0 11] 0.558529 []
[ 12 30] 2.023606 the [the]
[ 31 106] 2.249496 shower [shower]
[ 107 108] -2.753532
[
]
re-computed AM score: 207.983871
=== end forced alignment ===

——
### read waveform input
1 files processed

音声認識をやってるっぽいのはわかるけど、Google-cloud-speechとは大分違うな

$ sudo vi mic.jconf

-input mic
-htkconf wav_config
-h ENVR-v5.3.am
-hlist ENVR-v5.3.phn
-d ENVR-v5.3.lm
-v ENVR-v5.3.dct
-b 4000
-lmp 12 -6
-lmp2 12 -6
-fallback1pass
-multipath
-iwsp
-iwcd1 max
-spmodel sp
-no_ccd
-sepnum 150
-b2 360
-n 40
-s 2000
-m 8000
-lookuprange 5
-sb 80
-forcedict

$ ../julius/julius/julius -C mic.jconf -dnnconf dnn.jconf
Notice for feature extraction (01),
*************************************************************
* Cepstral mean and variance norm. for real-time decoding: *
* initial mean loaded from file, updating per utterance. *
* static variance loaded from file, apply it constantly. *
* NOTICE: The first input may not be recognized, since *
* cepstral mean is unstable on startup. *
*************************************************************
Notice for energy computation (01),
*************************************************************
* NOTICE: Energy normalization is activated on live input: *
* maximum energy of LAST INPUT will be used for it. *
* So, the first input will not be recognized. *
*************************************************************

——
### read waveform input
Stat: adin_oss: device name = /dev/dsp (application default)
Error: adin_oss: failed to open /dev/dsp
failed to begin input stream

使い方がイマイチよくわからん
全部コマンドラインでアウトプットが出てくるんかいな

[エンジニアの電話営業] コツ1

1. 嫌がられるのに負けない心を持つこと
 L 遠回しに言わずに簡潔に
 L 相手の気持ちを考える
 L セールスっぽくならないように
 -> モチベーションを保つ工夫をすること : 目標を達成したら美味いもの食べる、興味ある業界に営業する
 -> 何歳くらいの、どういう人が出たか記録を付けて分析する

2. 同業種の営業マンに聞いたり、話を振ってみる
      丁寧に言葉遣いを気をつける
「〜ご検討いただけないかと思いましてですね」と言っているので、「〜ご検討いただけないでしょうかと思いご連絡させていただきました。」にトークスクリプトを変更する

3. その他
 - リアクション芸人
– 声を大きく
 - メリットを伝える

モチベーション、精神面が一番大事だな。一番最初に一番大事な内容が来ました。

[GCP] Python3 x google-cloud-speechで音声認識を実装する

GCPでCloud Speech-to-Text APIを有効化します。

GCPユーザの認証データの*.jsonを取得します。

audio.rawはこちらからお借りします。
https://github.com/googleapis/java-speech/tree/master/samples/install-without-bom/resources

$ pip3 install google-cloud-speech

main.py

import os
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "hpscript.json"
from google.cloud import speech
import io

def transcribe_file(speech_file):
	
    client = speech.SpeechClient()

    with io.open(speech_file, "rb") as audio_file:
        content = audio_file.read()

    audio = speech.RecognitionAudio(content=content)
    config = speech.RecognitionConfig(
        encoding=speech.RecognitionConfig.AudioEncoding.LINEAR16,
        sample_rate_hertz=16000,
        language_code="en-US",
    )
    response = client.recognize(config=config, audio=audio)

    for result in response.results:
        print(u"Transcript: {}".format(result.alternatives[0].transcript))

if __name__ == '__main__':
	transcribe_file('audio.raw')

$ python3 main.py
Transcript: how old is the Brooklyn Bridge

うーむ、なんだこれは