つよく、やさしく、泥臭く生きていくブログ

日常とポエムと、ときどき技術

Acmeモジュールを書いた

Acmeデビューしました

metacpan.org

Acme::KemonoFriends::Color というモジュールです。
printkという関数をエクスポートします。printkを使うと、けものフレンズっぽい色でprintします。

SYNOPSISに書いてあるとおりこんな感じに書くと、

use Acme::KemonoFriends::Color;
use utf8;

printk('hogehoge');

$ perl script.pl

f:id:yukinea:20190305014904j:plain
たーのしー!

ターミナルが256色表示に対応している必要があります。
Tera Termでは 設定->ウィンドウ から、「256色モード」のチェックボックスがあると思います。

どんな色が使えるのかは、
https://misc.flogisoft.com/bash/tip_colors_and_formatting
このページが見やすかったです。
このページと、けもフレ公式ページのロゴとを見比べて一番近いっぽい色を選びました。

本当はprint関数をオーバーライドするようにして、useするだけでprintが色付くみたいにしたかったんですけど、print関数ってオーバーライドできない(?)んですね。

雑に確認したところ同じようなものは無かったけど、なんかすごく既にありそうで怖い。

参考にしたページ http://gihyo.jp/dev/serial/01/perl-hackers-hub/005001

Mojolicious::LiteとDBIx::Classを使って簡単なwebアプリケーションを作る

  • 社内の勉強会で、「PerlでDBを扱う」というのを教える必要があったので、その時の資料の抜粋を書き留めておく。

  • 内容としては、

    • DBIx::Classの基本的な使い方をさらった後、
    • Mojolicious::Liteで簡単な書籍管理アプリを作り、DB操作を盛り込むというハンズオン。
  • 後半にある「Webアプリ上に実現してみよう」を順にこなすとwebアプリケーションができあがります。
  • 詳しく動作を理解するとか実践的な内容というよりも、Perlでこういうことがサクッとできますよ、という紹介です。
  • 弊社のPerl 人口は風前の灯である。

  • 想定対象者としては、Perl初心者、プログラミング初心者で、Perlの基本文法は(一応)分かり、(調べながらでも)fizzbuzz問題は書けるくらいの人

  • また、Mojolicious::Liteによるwebアプリを作るので、Perl入学式の第5回資料をさらった人。(弊社勉強会ではこれの前段階でPerl入学式の第5回資料を実施しました。)

github.com

注意

  • 勉強会のための方便的な記述や雑な記述があります
  • 足りないところは喋りで解説

準備

$ cpanm -l ~/extlib local::lib
$ perl -I ~/extlib/lib/perl5 -Mlocal::lib=~/extlib | tee -a ~/.bash_profile
$ exec $SHELL -l

$ cpanm Mojolicious
$ cpanm DBIx::Class
$ cpanm DBD::SQLite
$ cpanm DBIx::Class::Schema::Loader

DB (SQLite)の初期準備

  • DBにはいくつかの種類がありますが、今回は手軽さのためSQLiteを使います。
  • SQLiteはデータ保存先として単一のファイルを使います。
$ sqlite3 sqlite.db

# booksという名前のテーブル作成
sqlite> CREATE TABLE books(id INTEGER PRIMARY KEY AUTOINCREMENT, author text, title text, price integer);

# 適当にレコードを入れる
sqlite> INSERT INTO books (author, title, price) VALUES ('Dazai Osamu', 'Hashire merosu', '1111');
sqlite> INSERT INTO books (author, title, price) VALUES ('Dazai Osamu', 'Ningen shikkaku', '2222');
sqlite> INSERT INTO books (author, title, price) VALUES ('Miyazawa Kenji', 'Ginga tetsudo no yoru', '1234');

# SELCT結果を見やすく整形する
sqlite> .header ON
sqlite> .mode column

# SELECTしてみる
sqlite> select * from books;
id          author       title           price
----------  -----------  --------------  ----------
1           Dazai Osamu  Hashire merosu  1111
2           Dazai Osamu  Ningen shikkak  2222
3           Miyazawa Ke  Ginga tetsudo   1234

DB操作

スキーマ情報を取得する

  • 参考:

  • DB操作といえばSQL文が一般的かと思います。

  • Perlでも、SQL文をコードに書いてDB操作を行うことができます。
  • ただし今回使うDBIx::Classは、ORM(O/Rマッパー)と呼ばれるもので、SQL文を書かずに使えます。
  • DB操作をオブジェクトのように扱えます。
  • DBIx::Classは、事前に「データベース」に対応したクラス(スキーマクラス)や、「テーブル」に対応したクラスを用意しておき、プログラムではそいつらを通して DB にアクセスします。
  • このスキーマクラスは、既存のDBから自動で作成することができます。
スキーマ作成するためのスクリプト
  • これはDBに変更がなければ初回に一回やるだけでよいものです
以下を make_schema.pl として保存する
#!/usr/bin/perl
use strict;
use warnings;
use FindBin;
use File::Spec;
use DBIx::Class::Schema::Loader qw/make_schema_at/;

make_schema_at(
    'MyDB::Schema',
    {
        # 出力先のディレクトリ
        dump_directory => File::Spec->catfile( $FindBin::Bin ),
        really_erase_my_files => 1,
        debug => 1,
    },
    [
        'dbi:SQLite:dbname=sqlite.db',
        {
            on_connect_do => ['SET NAMES utf8'],
        }
    ]
);
$ perl make_schema.pl
これで自分のディレクトリにMyDBというディレクトリができてその中にいろいろできていればOK

DBに接続する

  • DBに接続してDBの情報を取得してみましょう
  • 以下を db.pl として保存してください
#!/usr/bin/perl
use strict;
use warnings;

use MyDB::Schema;

# スキーマクラスのインスタンスを作成
my $schema = MyDB::Schema->connect(
    "DBI:SQLite:dbname=sqlite.db"
);

# ResultSet を作成し全レコード取得
my $rs = $schema->resultset('Book')->search;


# イテレートして出力
while (my $row = $rs->next) {
    printf "%s : %s : %s" , $row->id , $row->author, $row->title;
    print "\n";
}

検索 (search)

  • search で、authorを検索
  • 先ほどsearchしていた箇所をこのように変えてみましょう
# author がDazai Osamu のものを検索
my $rs = $schema->resultset('Book');

my $result=$rs->search(
    {
        author => 'Dazai Osamu',
    }
);
  • ライク検索
# author がDazai何とか のものを検索
my $rs = $schema->resultset('Book');

my $result=$rs->search(
    {
        author => { like => "%Dazai%" },
    }
);
  • OR検索
# author がMitazawa何とか or タイトルがNingen shikkakuのものを検索
my $rs = $schema->resultset('Book');

my $result=$rs->search(
    {
        -or => {
            author => { like => "Miyazawa%" },
            title  => "Ningen shikkaku",
        },
    }
);
  • AND検索
# author がDazai何とかで、タイトルがNingen何とかのものを検索
my $rs = $schema->resultset('Book');

my $result=$rs->search(
    {
        -and => {
            author => { like => "Dazai%" },
            title  => { like => "Ningen%"},
        },
    }
);
  • ANDとORの組み合わせ
# ちょっと複雑な検索条件
# (authorがDazai何とか かつ titleがNingenなんとか ) または authorがMiyazawa Kenji  を検索
my $rs = $schema->resultset('Book');

my $result=$rs->search(
    {
        -or => {
            -and => {
                author => { like => "Dazai%" },
                title  => { like => "Ningen%"},
            },
            author => 'Miyazawa Kenji',
        },
    },
);
  • ORDER BY (並び替え)
# 全件検索して、値段順に並び変える
my $rs = $schema->resultset('Book');

my $result=$rs->search(
    {
        # 検索条件は空 なので 全件取得
    },
    {
        # オプションを付けられる。order_byは順序
        order_by => ['price'],
    }
);

# 出力のところで値段も出すようにしましょう
while (my $row = $result->next) {
    printf "%s : %s : %s : %s" , $row->id , $row->author, $row->title, $row->price;
    print "\n";
}

行追加、変更

  • CREATE (追加)
my $rs = $schema->resultset('Book');

$rs->create(
    {
        author => 'Matayoshi',
        title  => 'Hibana',
        price  => 999,
    }
);

my $result = $rs->search();

# イテレートして出力
while (my $row = $result->next) {
    printf "%s : %s : %s : %s" , $row->id , $row->author, $row->title, $row->price;
    print "\n";
}
  • UPDATE
my $rs = $schema->resultset('Book');

# 変更対象のレコードを取得して
my $id4 = $rs->search(
    { id => 4 }
);

# そいつをアップデート
$id4->update(
    {
        title => 'Kinikuman',
        author => 'Yudetamago'
    }
);

my $result = $rs->search();
# イテレートして出力
while (my $row = $result->next) {
    printf "%s : %s : %s : %s" , $row->id , $row->author, $row->title, $row->price;
    print "\n";
}
  • メソッドチェーンのようにつなげられる
my $rs = $schema->resultset('Book');

$rs->search(
    { id => 4 }
)->update(
    {
        title => 'Kinikuman',
        author => 'Yudetamago'
    }
);

my $result = $rs->search();
  • 複数レコードをまとめて処理することも可能
# Dazai Osamu のレコード全てpriceを変更
$rs->search(
    {
        author => 'Dazai Osamu',    # Dazaiのレコードは2つある
    }
)->update(
    {
        price  => 99999
    }
);
  • イテレートする中でupdateしたりもできる
my $rs = $schema->resultset('Book');

my $result = $rs->search();
# イテレートして、それぞれupdateして、出力
while (my $row = $result->next) {

    my $price = $row->price;
    $price = $price * 3;    # 現在の値段の3倍をとって

    # その行を更新する
    $row->update(
        {
            price => $price,
        }
    );

    printf "%s : %s : %s : %s" , $row->id , $row->author, $row->title, $row->price;
    print "\n";
}

行削除

  • DELETE
my $rs = $schema->resultset('Book');

$rs->search(
    { id => 5 }
)->delete();

Webアプリ上に実現してみよう

  • DB操作を盛り込んだWebアプリを作ってみましょう。
  • 簡単な書籍管理アプリです。
    • 一覧の表示
    • 検索した結果の表示
    • 書籍情報の追加
    • 変更、削除
  • Mojolicious::Liteを使って作ります。

Mojolicious::Liteの準備

  • ひな形作成
$ mojo generate lite_app books_list.pl

一覧表示

  • /にアクセスされたときに、booksテーブルの中身を全部表示します。
  • use MyDB::Schema;を追加
  • get / の中身を変更
  • テンプレートの変更
    • index.html.epの変更
    • default.html.epで、bootstrapを使うように追記

GET /

#!/usr/bin/env perl
use Mojolicious::Lite;
use MyDB::Schema;

get '/' => sub {
  my $c = shift;

  my $schema = MyDB::Schema->connect(
      "DBI:SQLite:dbname=sqlite.db",
  );

  my $rs = $schema->resultset('Book');
  my $result = $rs->search();

  $c->stash(result => $result);
  $c->render(template => 'index');
};

app->start;
  • まずは、コントローラの中身を変更します。
  • テーブルの全レコードを取得して、stashを使い、テンプレートへ渡します。
@@ index.html.ep
% layout 'default';
% title 'Books List';

<div class="container">
  <div class="row border-bottom border-dark bg-light">
    <div class="col-sm"> id     </div>
    <div class="col-sm"> author </div>
    <div class="col-sm"> title  </div>
    <div class="col-sm"> price  </div>
  </div>

% while (my $row = $result->next) {
  <div class="row border-bottom">
    <div class="col-sm"> <%= $row->id     %> </div>
    <div class="col-sm"> <%= $row->author %> </div>
    <div class="col-sm"> <%= $row->title  %> </div>
    <div class="col-sm"> <%= $row->price  %> </div>
  </div>
% }

</div>

@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
  </head>
  <body><%= content %></body>
</html>
  • index.html.epと,default.html.epをそれぞれ変更します。
  • indexでは、コントローラから渡された$resultを使いながらhtmlを記述しています。
  • defaultでは、CSSフレームワークのbootstrapを読み込むようにしています。
    • これで見た目をサクッときれいにしちゃいます。
  • ここまで出来たら、morboを起動して表示を確認しましょう。
$ morbo books_list.pl

検索

  • フォームを作り、入力された値を使って検索した結果をテンプレートに渡します

post /search

post '/' => sub {
  my $c = shift;

  # POSTされた値を取る
  my $author = $c->param('author');
  my $title  = $c->param('title');

  # POSTされた値をもとに、検索条件を作る
  my $query = {};
  if ( $author ) {
      $query->{author} = { like => "%$author%"};
  }
  if ( $title ) {
      $query->{title} = { like => "%$title%"};
  }

  my $schema = MyDB::Schema->connect(
      "DBI:SQLite:dbname=sqlite.db",
  );

  # 検索する
  my $rs = $schema->resultset('Book');
  my $result = $rs->search(
      {
          -and => $query,
      }
  );

  $c->stash(result => $result);
  $c->render(template => 'index');

};
  • post '/' を新たに追記します。
  • schemaをとるところがgetで書いたものと冗長ですね。後で直します。
%= form_for '/' => method => 'POST' => begin
  <div class="col-md-3">
    著者名 <%= text_field 'author' , class => 'form-control' %>
  </div>
  <div class="col-md-3">
    作品名 <%= text_field 'title' , class => 'form-control' %>
  </div>
  %= submit_button '検索' , class => 'btn btn-primary mt-3'
% end
  • 先ほどのindex.html.epに、フォーム部分を追記します。
  • form_forといった機能を使っていますが、普通に
    タグなどで書いてもかまいません。
  • ここまで書けたら、morboを起動し、検索を実行してみましょう。

サブルーチンへの切り出し

#!/usr/bin/env perl
use Mojolicious::Lite;
use MyDB::Schema;

sub get_schema {
  return MyDB::Schema->connect(
      "DBI:SQLite:dbname=sqlite.db",
  );
}

sub get_book_rs {
    return get_schema()->resultset('Book');
}

get '/' => sub {
  my $c = shift;

  my $rs = get_book_rs();
  my $result = $rs->search();
  $c->stash(result => $result);

  $c->render(template => 'index');
};

post '/' => sub {
  my $c = shift;

  my $author = $c->param('author');
  my $title  = $c->param('title');

  my $query = {};
  if ( $author ) {
      $query->{author} = { like => "%$author%"};
  }
  if ( $title ) {
      $query->{title} = { like => "%$title%"};
  }

  my $rs = get_book_rs();
  my $result = $rs->search(
      {
          -and => $query,
      }
  );
  $c->stash(result => $result);
  $c->render(template => 'index');
};

app->start;
  • get と postでどちらも schemaを取るところは同じなので、サブルーチンにまとめます。

書籍追加機能

  • 追加するためのページを一枚用意します

GET /add

get '/add' => sub {
  my $c = shift;
  $c->render(template => 'add');
};
中略
  %= submit_button '検索' , class => 'btn btn-primary mt-3'
% end
<a class="btn btn-warning" href="<%= url_for('/add') %>">新規追加</a>

@@ add.html.ep
% layout 'default';
% title 'Books List add';

%= form_for '/add' => method => 'POST' => begin
  <div class="col-md-3">
    著者名 <%= text_field 'author' , class => 'form-control' %>
  </div>
  <div class="col-md-3">
    作品名 <%= text_field 'title' , class => 'form-control' %>
  </div>
  <div class="col-md-3">
    価格 <%= text_field 'price' , class => 'form-control' %>
  </div>
  %= submit_button '追加する' , class => 'btn btn-primary mt-3'
% end
  • 検索フォームのすぐ下に、新規追加画面へのリンクを置きます。
  • add.html.epを追記します。
    • フォームで追加ボタンを押すと、/add へPOSTします

POST /add

get '/add' => sub {
  my $c = shift;
  $c->render(template => 'add');
};

post '/add' => sub {
  my $c = shift;

  my $author = $c->param('author');
  my $title  = $c->param('title');
  my $price  = $c->param('price');

  # 本来ならこの辺で入力値のチェックをするべき

  my $query = {
      author => $author,
      title => $title,
      price => $price,
  };

  my $rs = book_rs();
  $rs->create( $query );

  $c->redirect_to('/');
};
  • 新規追加でPOSTされてきたときに、DBへ追加します。
  • 追加処理だけしたあとは、index(/)へリダイレクトします、
  • 本来は、入力された値は妥当かどうかをチェックし、妥当でなければその旨を表示させるのが一般的です。

日本語対応

  • SQLiteに日本語が入っていて文字化けする場合、このようにする
use Encode qw( decode );

# テンプレート内で使えるhelperという関数を定義する
helper decode_utf8 => sub {
    my ($self,$string) = @_;
    my $string_utf8 = decode ('utf-8', $string);
    return $string_utf8;
};

中略
テンプレート内で、DBの内容で日本語がありうるところにdecode_utf8を入れる
    <div class="col-sm"> <%= decode_utf8( $row->author ) %> </div>
    <div class="col-sm"> <%= decode_utf8( $row->title )  %> </div>

変更

GET /edit/:id

get '/edit/:id' => sub {
  my $c = shift;
  my $id = $c->param('id');

  my $rs = get_book_rs();
  my $result = $rs->search(
      { id => $id }
  )->first;

  $c->stash(row => $result);
  $c->render(template => 'edit');
};
  • get '/edit/:id' を追加します。
  • /edit/:id の「:id」はプレースホルダと呼ばれるもので、URLの一部を取得することができます。
  • 取得した部分は、 my $id = $c->param('id'); で取り出します。
<div class="container">
  <div class="row border-bottom border-dark bg-light">
    <div class="col-sm">        </div>
    <div class="col-sm"> id     </div>
    <div class="col-sm"> author </div>
    <div class="col-sm"> title  </div>
    <div class="col-sm"> price  </div>
  </div>

% while ( my $row = $result->next ) {
  <div class="row border-bottom">
    <a class="btn btn-light" href="<%= url_for('/edit/' . $row->id) %>">変更</a>
    <div class="col-sm"> <%= $row->id    %> </div>
    <div class="col-sm"> <%= decode_utf8( $row->author ) %> </div>
    <div class="col-sm"> <%= decode_utf8( $row->title )  %> </div>
    <div class="col-sm"> <%= $row->price  %> </div>
  </div>
% }

</div>
@@ edit.html.ep
% layout 'default';
% title 'Books List edit';

%= form_for '/edit/'. $row->id => method => 'POST' => begin
  <div class="col-md-3">
    著者名
    <%= text_field 'author' => decode_utf8($row->author) , class => 'form-control' %>
  </div>
  <div class="col-md-3">
    作品名
    <%= text_field 'title'  => decode_utf8($row->title) , class => 'form-control' %>
  </div>
  <div class="col-md-3">
    価格
    <%= text_field 'price' => $row->price , class => 'form-control' %>
  </div>
  %= submit_button '変更する' , class => 'btn btn-primary mt-3'
% end
  • index.html.ep に変更ボタンを付けます。
  • 表示されるボタンのURLに注目してください。
  • また、edit.html.epテンプレートを作成します。

POST /edit/:id

post '/edit/:id' => sub {
  my $c = shift;
  my $id = $c->param('id');

  my $author = $c->param('author');
  my $title  = $c->param('title');
  my $price  = $c->param('price');

  # 本来ならこの辺で入力値のチェックをするべき

  my $query = {
      author => $author,
      title => $title,
      price => $price,
  };

  # 指定したid のレコードを更新
  my $rs = get_book_rs();
  my $result = $rs->search(
      { id => $id }
  )->update( $query );

  $c->redirect_to('/');
};
  • post '/edit/:id' を追加します。
  • 受け取った値で、レコードを更新します
  • 更新処理した後は、indexページへリダイレクトします。

削除

POST /delete/:id

post '/delete/:id' => sub {
  my $c = shift;
  my $id = $c->param('id');
  my $rs = get_book_rs();
  my $result = $rs->search(
      { id => $id }
  )->delete;

  $c->redirect_to('/');
};
  • post '/delete/:id' を追加する
  • delete処理をした後はindexページへリダイレクトします
@@ edit.html.ep
% layout 'default';
% title 'Books List edit';

%= form_for '/edit/'. $row->id => method => 'POST' => begin
  <div class="col-md-3">
    著者名
    <%= text_field 'author' => decode_utf8($row->author) , class => 'form-control' %>
  </div>
  <div class="col-md-3">
    作品名
    <%= text_field 'title'  => decode_utf8($row->title) , class => 'form-control' %>
  </div>
  <div class="col-md-3">
    価格
    <%= text_field 'price' => $row->price , class => 'form-control' %>
  </div>
  %= submit_button '変更する' , class => 'btn btn-primary mt-3'
% end

%= form_for '/delete/'. $row->id => method => 'POST' => begin
  %= submit_button '削除する' , class => 'btn btn-danger mt-3'
% end
  • editテンプレート内に、削除ボタンを追記します。

参考

  • 今回作成の最終的なアプリ
    books_list.pl
#!/usr/bin/env perl
use Mojolicious::Lite;
use MyDB::Schema;
use Encode qw ( encode decode ); 

sub get_schema {
  return MyDB::Schema->connect(
      "DBI:SQLite:dbname=sqlite.db",
  );
}

sub get_book_rs {
    return get_schema()->resultset('Book');
}

# テンプレート内で簡単に使える関数を定義できる
helper decode_utf8 => sub {
    my ($self,$string) = @_;
    my $string_utf8 = decode ('utf-8', $string);
    return $string_utf8;
};

# /にGETアクセスされたときの処理
get '/' => sub {
  my $c = shift;

  my $rs = get_book_rs();
  my $result = $rs->search();
  $c->stash(result => $result);

  $c->render(template => 'index');
};

# /にPOSTアクセスされたときの処理
post '/' => sub {
  my $c = shift;

  my $author = $c->param('author');
  my $title  = $c->param('title');

  my $query = {};
  if ( $author ) {
      $query->{author} = { like => "%$author%"};
  }
  if ( $title ) {
      $query->{title} = { like => "%$title%"};
  }

  my $rs = get_book_rs();
  my $result = $rs->search(
      {
          -and => $query,
      }
  );
 # 上記は変数展開されるとこうなる
#    {
#        -and => {
#            author => { like => "%$author%"},    # ここと
#            title     => { like => "%$title%"}   # ここが $queryの中身
#        }
#    }

  $c->stash(result => $result);
  $c->render(template => 'index');
};

get '/add' => sub {
  my $c = shift;
  $c->render(template => 'add');
};

post '/add' => sub {
  my $c = shift;

  my $author = $c->param('author');
  my $title  = $c->param('title');
  my $price  = $c->param('price');

  # 本来ならこの辺で入力値のチェックをするべき

  my $query = {
      author => $author,
      title => $title,
      price => $price,
  };

  my $rs = get_book_rs();
  $rs->create( $query );

  # DBに入れる処理だけして
  # TOPページに飛ばす
  $c->redirect_to('/');
};


# /edit/:hoge/ という「:hoge」のような書き方をすると、URLの一部を取得できる
get '/edit/:id' => sub {
  my $c = shift;
  my $id = $c->param('id');    # http://x.x.x.x:3000/edit/12345  だと、12345が取れる

  my $rs = get_book_rs();
  my $result = $rs->search(
      { id => $id }
  )->first;

  $c->stash(row => $result);
  $c->render(template => 'edit');
};

post '/edit/:id' => sub {
  my $c = shift;
  my $id = $c->param('id');

  my $author = $c->param('author');
  my $title  = $c->param('title');
  my $price  = $c->param('price');

  # 本来ならこの辺で入力値のチェックをするべき

  my $query = {
      author => $author,
      title => $title,
      price => $price,
  };

  # 指定したid のレコードを更新
  my $rs = get_book_rs();
  my $result = $rs->search(
      { id => $id }
  )->update( $query );

  $c->redirect_to('/');
};

post '/delete/:id' => sub {
  my $c = shift;
  my $id = $c->param('id');
  my $rs = get_book_rs();
  my $result = $rs->search(
      { id => $id }
  )->delete;

  $c->redirect_to('/');
};

app->start;

__DATA__

@@ index.html.ep
% layout 'default';
% title 'Books List';

%= form_for '/' => method => 'POST' => begin
  <div class="col-md-3">
    著者名 <%= text_field 'author' , class => 'form-control' %>
  </div>
  <div class="col-md-3">
    作品名 <%= text_field 'title' , class => 'form-control' %>
  </div>
  %= submit_button '検索' , class => 'btn btn-primary mt-3'
% end
<a class="btn btn-warning" href="<%= url_for('/add') %>">新規追加</a>

<div class="container">
  <div class="row border-bottom border-dark bg-light">
    <div class="col-sm">        </div>
    <div class="col-sm"> id     </div>
    <div class="col-sm"> author </div>
    <div class="col-sm"> title  </div>
    <div class="col-sm"> price  </div>
  </div>

% while ( my $row = $result->next ) {
  <div class="row border-bottom">
    <a class="btn btn-light" href="<%= url_for('/edit/' . $row->id) %>">変更</a>
    <div class="col-sm"> <%= $row->id    %> </div>
    <div class="col-sm"> <%= decode_utf8( $row->author ) %> </div>
    <div class="col-sm"> <%= decode_utf8( $row->title )  %> </div>
    <div class="col-sm"> <%= $row->price  %> </div>
  </div>
% }

</div>

@@ add.html.ep
% layout 'default';
% title 'Books List add';

%= form_for '/add' => method => 'POST' => begin
  <div class="col-md-3">
    著者名 <%= text_field 'author' , class => 'form-control' %>
  </div>
  <div class="col-md-3">
    作品名 <%= text_field 'title' , class => 'form-control' %>
  </div>
  <div class="col-md-3">
    価格 <%= text_field 'price' , class => 'form-control' %>
  </div>
  %= submit_button '追加する' , class => 'btn btn-primary mt-3'
% end


@@ edit.html.ep
% layout 'default';
% title 'Books List edit';

%= form_for '/edit/'. $row->id => method => 'POST' => begin
  <div class="col-md-3">
    著者名
    <%= text_field 'author' => decode_utf8($row->author) , class => 'form-control' %>
  </div>
  <div class="col-md-3">
    作品名
    <%= text_field 'title'  => decode_utf8($row->title) , class => 'form-control' %>
  </div>
  <div class="col-md-3">
    価格
    <%= text_field 'price' => $row->price , class => 'form-control' %>
  </div>
  %= submit_button '変更する' , class => 'btn btn-primary mt-3'
% end

%= form_for '/delete/'. $row->id => method => 'POST' => begin
  %= submit_button '削除する' , class => 'btn btn-danger mt-3'
% end

@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title><%= title %></title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
  </head>
  <body><%= content %></body>
</html>

参考: http://nekokak.jf.land.to/wiki/wiki.cgi/sub?page=Perl%2FDBIC

あけましておめでとうございます。第2019回

あけましておめでとうございます。
1月1日0時からFGOイベントが始まって忙しいです。

いよいよ平成残すところわずかとなりましたがここで昨年を振り返りましょう。

1月

Kindle買ったんすね。結局あんまり使ってないな、、 確かこの辺で流行りに乗って仮想通貨ちょっと買ったんすよ(察し

2月

寝屋川ハーフマラソンを完走。 目標の2時間半切り達成できてよかった

3月

女の子と一緒にプリキュア映画見に行った

4月

仕事が激動でしたね。いや、やってる業務は変わらないんですけど。
チーム(実働4人)のリーダー的ポジションの人が異動になって自分がリーダーポジションになりました。
そして一人は別チームに持っていかれて実働3人になりました。 3人のうち一人は元リーダーの後任でやってきた人なので、まずその人にいろいろ教えたりできるところからやってもらったりっていうあたりで頭使いました。

5月

ゴールデンウィークにアマプラでふたりはプリキュアを全話見ました。改めてみると最高に最高で最高って感じでまさにレジェンドだった。

6月

地震ありましたねーびっくりしました。
なぜか毎日パスタを作ってました。 イタリアンのレシピ本買ったからね。

7月

このくらいだったかな、4月に来てたチームメンバーが別チームに持っていかれて、実働2人になりました。業務は変わっていません。むしろ増え気味です。

8月

富士山登った!ご来光見たよ。
いいもんですね。一生に一度は日本のてっぺん行くの良いと思いますよ。 めっちゃ寒かったね。

9月

台風でしたねーびっくりしました。
停電は1時間くらい×2回くらいだけだったんですけど、なぜかこんなときに冷蔵庫の扉が閉まりきってなくてね、次の日部屋がめっちゃ寒くて、「台風過ぎ去ったら急に冬が来たのか?」と思いました。

10月

女の子とプリキュア映画観に行きました。
いやこの映画最高だったんでみんな観てください。

11月

夜の公園でランニングと筋トレを始めました。なんとか継続しています。月火木だけやってます。コツはちょっとしかやらないこと。公園を一周して、鉄棒とぶら下がるやつで少しの筋トレ。最後に公園を一周しておしまい。 家を出てから家に戻ってくるまで30分くらいだけです。

12月

朝ごはん食べるのが習慣化してきました。良い生活をしている感が出て良いです。

今年もソラーレの定演が無事に終わった。
1stはつらいよ

IBEの演奏会聴いてすげーなーと思ってトランペット(2万くらい)をポチった。

といった感じ。

仕事面においては、リーダポジション&削られるチーム&新案件いろいろってな感じで、どうすりゃいいのかなーと悩む一年でした。
そろそろ慣れてきたので図太い神経で生きていきたい。

プライベートに関しては、まあ最近筋トレと朝ごはんの習慣が付いてこれは良い。 あとはもう少し人間面と技術面での向上というか、前向きになりたいっすね。

楽器に関しては、音を出す基礎をちゃんとするのと、スケール練習をしっかりやっていきたい

平成最後の平成31年頑張っていくぞ!

たくさんのWebページのSSL証明書の有効期限を確認する

ちょっと大量のドメインSSL証明書を確認する必要があった。

openssl コマンドで確認する

$ openssl s_client -connect example.com:443 -showcerts < /dev/null 2>/dev/null |  openssl x509 -dates -noout

各部分の説明

$ openssl s_client -connect ${domain}:443 -showcerts

このコマンドで、証明書情報とれる。
ただし、このコマンドだけでは接続中の状態になるので、 Ctrl + cとかで終わらせないと、次のパイプにつなげたときに上手くいかない。
ので、

< /dev/null 2>/dev/null 

を付けて、終わらす&エラー出力は不要なので捨てる。 これ分かるまでなんかうまいことできなくて詰まった。

 |  openssl x509 -dates -noout

さっきのをパイプで繋げて証明書の中身を見る。
-datesを付けると有効期限とかが出てくる。notAfter=hogehoge って感じで書かれてるのが、有効期限である。

perl で回す

さっきので1ドメイン分が取れるので、普通にシェルの for で回してもいいんだけど、出力を適当に加工したりするかもしれないので融通の利くperlで回す。

for my $domain ( @domain_list ) {
    my $result = check( $domain ) // 'error';
    print "$result : $domain \n";
}

sub check {
    my $domain = shift;
     my $command = "openssl s_client -connect ${domain}:443 -showcerts < /dev/null 2>/dev/null |  openssl x509 -dates -noout";
     my $result = `$command`;
     chomp $result;
 
     if ( $result =~ /notAfter=(.*)/  ) {
         return $1;
     }
}

参考

Perl で SSL 証明書の有効期限を取得する : あかぎメモ

OpenSSLをSSL/TLSクライアントとして使ってみる | Siguniang's Blog

postgresqlを使う

heroku でDB使うならとりあえずheroku postgresかなあというところで、さてpostgresはろくに触ったことがない。
とりあえずローカル環境にposgresを用意した。

これを参考 UbuntuでPostgreSQLをインストールからリモートアクセスまでの手順 - Qiita

  • Ubuntu環境
  • インストール
    $ sudo apt-get install postgresql
    postgresqlをインストールすると、heroku pg:psql -c "\d" とかできるようになった。
    もちろんテーブルとか作ってないので何か出るわけではない。
    ローカルでアプリの動作確認するにはローカルのposgre DBを用意する必要があるので次に進む。
  • postgres サーバ立ち上げ
    $ sudo service postgresql start
  • psql コマンドでpostgresのプロンプトに入る
    $ sudo -s
    # su - postgres
    postgres@VB:~$ psql
    postgresはRoleというものがあり、権限がなければ入れない。
    初期状態では postgres という名前が用意されている。(postgresをインストールするとpostgresユーザが作られる)
    postgres のパスワードが分らなかったのでroot経由で行った。

  • ロール確認・作成
    postgres=# \du
    postgres=# CREATE ROLE yukinea LOGIN CREATEDB PASSWORD 'hogehoge';

  • DB確認・作成
    postgres=# \l
    postgres=# CREATE DATABASE testdb OWNER yukinea;

  • 認証の設定
    $ sudo vim /etc/postgresql/9.3/main/pg_hba.conf
    こんな感じで これのpeer のところをtrustに変更した

# local   all             all                                     peer
local   all             all                                     trust
  • postgres再起動
    $ sudo service postgresql restart

  • 接続
    $ psql -U yukinea --dbname testdb --password

Mojolicious::LiteでLINEログイン

LINEログインしてプロフィール取るところまでできたので忘れないように書いておく。

LINEログインをして、ユーザのuserid、表示名、プロフィール画像を取得して、表示名と画像を表示するところまで。

Perl で Mojolicious::lite でheroku にデプロイしている。
もろもろチェックやなんやらは省略している。

  • 実質のファイルは

    • myapp.pl
    • templates/index.html.ep
    • templates/layouts/default.html.ep
    • cpanfile
      requires 'Mojolicious'; requires 'IO::Socket::SSL', '>=2.009';
    • app.psgi (空っぽ)
    • Procfile
      web: starman --preload-app --port $PORT myapp.pl psgi
  • プロフィールをとるまでは、こんな感じ。

    • ログイン認証させて認可コードを取得する
    • 認可コードを使ってアクセストークンを取得する
    • アクセストークンを使ってプロフィールを取得する
  • ログイン認証させるURLにはいくつかパラメータの指定が必要。 そのうち state は正しいアクセスかどうかを確認するのに使う。 ログイン認証するURLに含めたstateの値と、ログイン後に返ってくるstateの値が一致することを、こっちのアプリ側で検証してやらないといけない。もちろんアクセスごとにランダムな値を設定しないといけない。
    これって要するにMojoliciousのcsrf_tokenを使えばいいんじゃねということで使った。
    ログインボタンのURLに含めるstate=にはcsrf_tokenを入れる。
    csrf_tokenの検証は、普通なら元からあるメソッドでできるが、今回は直に、返ってきたstateの値とこっちのcsrf_tokenの値を比較した。

  • もろもろの値は環境変数に入れておく。heroku側で環境変数を設定する。

#!/usr/bin/env perl
use lib './lib';
use lib './local/lib/perl5';
use Mojolicious::Lite;
use Mojo::UserAgent;

helper is_ok_csrf_token => sub {
    my $self = shift;
    my $csrf_token = $self->session->{csrf_token};
    my $state = $self->param('state'); # csrf_token
    return 1 if $csrf_token eq $state;
    return;
};

helper get => sub {
    my ($self, $arg) = @_;
    my $url    = $arg->{'url'};
    my $header = $arg->{'header'};

    my $ua = Mojo::UserAgent->new();
    my $tx = $ua->get($url => $header);

    if ( my $res = $tx->success ) {
        return $res->json;
    }
};

helper post => sub {
    my ($self, $arg) = @_;
    my $url   = $arg->{'url'};
    my $param = $arg->{'param'};

    my $ua = Mojo::UserAgent->new();
    my $tx = $ua->post($url => form => $param);

    if ( my $res = $tx->success ) {
        return = $res->json;
    }
};

helper get_profile => sub {
    my $self = shift;

    my $access_token = $self->get_access_token;
    my $param = {Authorization => "Bearer $access_token"};
    my $url = 'https://api.line.me/v2/profile';

    my $result_hash = $self->get({
        url    => $url,
        header => $param,
    }) or return;

    return $result_hash;
};

helper get_profile => sub {
    my $self = shift;

    my $access_token = $self->get_access_token;
    my $param = {Authorization => "Bearer $access_token"};
    my $url = 'https://api.line.me/v2/profile';

    my $result_hash = $self->get({
        url    => $url,
        header => $param,
    }) or return;

    return $result_hash;
};

helper get_access_token => sub {
    my $self = shift;
    my $code = $self->param('code');

    my $token_url = 'https://api.line.me/oauth2/v2.1/token';
    my $post_param = {
        grant_type    => 'authorization_code',
        code          => $code,
        redirect_uri  => $ENV{CALL_BACK_URL},
        client_id     => $ENV{CHANNEL_ID},
        client_secret => $ENV{CHANNEL_SECRET},
    };

    my $result_hash = $self->post({
        url   => $token_url,
        param => $post_param,
    }) or return;

    my $access_token = $result_hash->{'access_token'};
    return $access_token if $access_token;
    return;
};

get '/' => sub {
    my $c = shift;

    if ( $c->param('code') && $c->is_ok_csrf_token ) {
        my $profile_hash = $c->get_profile;
        $c->stash('profile_hash' => $profile_hash);
    }

    $c->render(template => 'index');
};

app->secrets([$ENV{MOJO_SECRETS}]) if defined $ENV{MOJO_SECRETS};
app->start;
% layout 'default';
% title 'Welcome';
% my $profile_hash = stash('profile_hash');
<h1>LINE ログインテスト</h1>

<div>
  <a href="https://access.line.me/oauth2/v2.1/authorize?response_type=code&client_id=<%= $ENV{CHANNEL_ID} %>&redirect_uri=<%= $ENV{CALL_BACK_URL} %>&state=<%= csrf_token %>&scope=profile">
LINEでログインする
  </a>

</div>

<div>
    printデバッグ用<%= stash('message') %>
</div>
<% if ($profile_hash) { %>
<div>
  <p><%= $profile_hash->{'displayName'} %></p>
  <img src="<%= $profile_hash->{'pictureUrl'} %>">
</div>
<% } %>

herokuにMojolicious

LINEのbot作りたいと思って、オウム返しするものはこのサンプルコードをherokuにデプロイしてやればできた。
https://github.com/line/line-bot-sdk-perl/blob/master/eg/echo.psgi

前回記事のやり方で、

をおけばOKだった。

でもやっぱりMojoliciousが使いたいというわけで、この辺を見ていた。

Mojolicious::Liteで作ったアプリをHerokuで動かす | nqou.net

「heroku Mojolicious」とかでググるとだいたいこれと似たようなのがヒットするんだけど、どうにも上手くいかなかった。

デプロイしてOpenAppすると「Internal Server Error」の表示。
ログを見ると、「Can't use an undefined value as a subroutine reference at /app/vendor/perl/lib/site_perl/5.22.0/Plack/Util.pm line 145.」っていう表示。
いろいろ試した結果、上手くいった。

結論 Procfile ではなく procfile としていたせい。
頭大文字じゃないとダメっぽい。

とりあえずMojolicious::liteのデフォルトページが表示された。
Mojoliciousの環境さえできればこっちのもんですわ。