【PHP】Carbon::now()は誰のnow?

こんにちは、毎日暑いですね。エンジニアの三宅です。

最近、仕事でPHPのコードリーディングを少ししたので、その過程を記載します!

Carbon::now()はOSの現在のシステム時刻

結論はこれです。OSのシステム時刻を持ってきています。

これ以降の内容は、コードを見ながら本当にシステム時刻から取ってきているのかを確認するためのものです。

Carbonとは

今回扱っているCarbonはPHPのDateTimeクラスを継承し、時間操作を簡易化したり便利化したものを提供する拡張クラスになります!

carbon.nesbot.com

これが所謂Carbon::now()の使用例

<?php
require 'vendor/autoload.php';
use Carbon\Carbon;

$now = Carbon::now();
$now->format('Y-m-d H:i:s'); // 2022-08-24 19:31:22

PHPの時間操作

DateTimeはPHP標準クラスです!

www.php.net

Carbonはこれを継承しています。

コードリーディング方針

今回は如何にWEB完結でコードリーディングしていくかの手順を書いていきますが、本格的に処理を追うのであれば debug実行出来る環境を手元に用意すると良いと思います。

WEBでコードリーディング実践

Carbonクラスのコードリーディング

では早速Carbonから追っていきましょう

まずはCarbonのgithubからCarbon::now();の処理を確認します。 github.com

GitHub上で検索する際には"function now"とダブルクォーテーションで括ります こうすると目指す関数に当たりやすいです!

ちなみに対象関数を探すのは、GitHub上よりもgit cloneしてローカルファイルをエディターで検索した方が楽です

ただ、GitHub検索するとissueやcommitメッセージなども対象になるため色んな情報を得る可能性があるというメリットもあります

検索の結果、どうやらここにあるようです!! github.com

nowメソッド

/**
     * Get a Carbon instance for the current date and time.
     *
     * @param DateTimeZone|string|null $tz
     *
     * @return static
     */
    public static function now($tz = null)
    {
        return new static(null, $tz); // ← ここでインスタンス生成して返している
    }

newしたものを返しているようです! new staticは実行されるクラスのインスタンスを生成します。

実行クラスのコンストラクタが第一引数nullで呼ばれますので、そちらの処理を確認

public function __construct($time = null, $tz = null)
    {
        if ($time instanceof DateTimeInterface) {
            $time = $this->constructTimezoneFromDateTime($time, $tz)->format('Y-m-d H:i:s.u');
        }

        if (is_numeric($time) && (!\is_string($time) || !preg_match('/^\d{1,14}$/', $time))) {
            $time = static::createFromTimestampUTC($time)->format('Y-m-d\TH:i:s.uP');
        }

        // If the class has a test now set and we are trying to create a now()
        // instance then override as required
        $isNow = empty($time) || $time === 'now';

        if (method_exists(static::class, 'hasTestNow') &&
            method_exists(static::class, 'getTestNow') &&
            static::hasTestNow() &&
            ($isNow || static::hasRelativeKeywords($time))
        ) {
            static::mockConstructorParameters($time, $tz);
        }

        // Work-around for PHP bug https://bugs.php.net/bug.php?id=67127
        if (!str_contains((string) .1, '.')) {
            $locale = setlocale(LC_NUMERIC, '0');
            setlocale(LC_NUMERIC, 'C');
        }

        try {

            // ここで親コンストラクタを呼んでいる!!!!!!
            parent::__construct($time ?: 'now', static::safeCreateDateTimeZone($tz) ?: null); 
        } catch (Exception $exception) {
            throw new InvalidFormatException($exception->getMessage(), 0, $exception);
        }

        $this->constructedObjectId = spl_object_hash($this);

        if (isset($locale)) {
            setlocale(LC_NUMERIC, $locale);
        }

        self::setLastErrors(parent::getLastErrors());
    }

親のコンストラクタを呼んでいます。

parent::__construct($time ?: 'now', static::safeCreateDateTimeZone($tz) ?: null); *

Carbonの親クラスは前述通りDateTimeクラスになります。 そもそも元のコンストラクタの第一引数である$timeはnullできているので、このCarbon→DateTimeのコンストラクタへ移行するタイミングで ’now’という引数が設定されることが読み取れます!

そうすると今度はDateTimeクラスになっていきます

DateTimeクラスのコードリーディング

DateTimeクラスのコンストラクタには説明ドキュメントがありました

www.php.net

PHPの元コードはC言語で書かれています!

GitHubで開発はされていないのですが、ミラーされたコードが上がっています。

github.com

まずは"class DateTime"で検索してみます。

C言語ではなくPHPのstabファイルで引っ掛かりました。

/**
 * Representation of date and time.
 * @link https://php.net/manual/en/class.datetime.php
 */
class DateTime implements DateTimeInterface
{
  ...
}

ここでファイルのディレクトリに注目!! dateを扱ってそうなディレクトリです。

php_date.cが怪しいなといって見ていきます。

今度はブラウザ検索で"PHP_FUNCTION"を見ていきます。

PHP_FUNCTIONC言語のマクロで定義されています。 機能は引数でもらった名前の関数をPHPで動作させるものです。

DateTime classのコンストラクタに当たりそうな処理が見つかりました!

github.com

コメントにReturns new DateTime objectとありますね!

/* {{{ Returns new DateTime object */
PHP_FUNCTION(date_create)
{
    zval           *timezone_object = NULL;
    char           *time_str = NULL;
    size_t          time_str_len = 0;

    ZEND_PARSE_PARAMETERS_START(0, 2)
        Z_PARAM_OPTIONAL
        Z_PARAM_STRING(time_str, time_str_len)
        Z_PARAM_OBJECT_OF_CLASS_OR_NULL(timezone_object, date_ce_timezone)
    ZEND_PARSE_PARAMETERS_END();

    php_date_instantiate(date_ce_date, return_value);
    if (!php_date_initialize(Z_PHPDATE_P(return_value), time_str, time_str_len, NULL, timezone_object, 0)) {
        zval_ptr_dtor(return_value);
        RETURN_FALSE;
    }
}
/* }}} */

上記コードはdate_create()というPHP関数の実行処理内容です!

www.php.net

date_create()を知っていれば、最初からdate_createで検索すればよかったのでは?と思うかと思います。

その通りです!

PHP関数名が分かっていればストレートに検索していけば問題なくたどり着けます!

さて、PHP_FUNCTION(date_create)の中を見てきます。。 php_date_initialize関数が正にその実行処理のようです! コードを追っていきます。

PHPAPI bool php_date_initialize(php_date_obj *dateobj, const char *time_str, size_t time_str_len, const char *format, zval *timezone_object, int flags) /* {{{ */
{
    timelib_time   *now;
    timelib_tzinfo *tzi = NULL;
    timelib_error_container *err = NULL;
    int type = TIMELIB_ZONETYPE_ID, new_dst = 0;
    char *new_abbr = NULL;
    timelib_sll new_offset = 0;
    time_t sec;
    suseconds_t usec;
    int options = 0;

    if (dateobj->time) {
        timelib_time_dtor(dateobj->time);
    }
    if (format) {
        if (time_str_len == 0) {
            time_str = "";
        }
        dateobj->time = timelib_parse_from_format(format, time_str, time_str_len, &err, DATE_TIMEZONEDB, php_date_parse_tzfile_wrapper);
    } else {
        if (time_str_len == 0) {
            time_str = "now";
            time_str_len = sizeof("now") - 1;
        }
        dateobj->time = timelib_strtotime(time_str, time_str_len, &err, DATE_TIMEZONEDB, php_date_parse_tzfile_wrapper);
    }

んー、まだnowの時間を取ってはなさそうですね..!

 /* update last errors and warnings */
    update_errors_warnings(err);

    /* If called from a constructor throw an exception */
    if ((flags & PHP_DATE_INIT_CTOR) && err && err->error_count) {
        /* spit out the first library error message, at least */
        zend_throw_exception_ex(NULL, 0, "Failed to parse time string (%s) at position %d (%c): %s", time_str,
            err->error_messages[0].position, err->error_messages[0].character, err->error_messages[0].message);
    }
    if (err && err->error_count) {
        timelib_time_dtor(dateobj->time);
        dateobj->time = 0;
        return 0;
    }

    if (timezone_object) {
        php_timezone_obj *tzobj;

        tzobj = Z_PHPTIMEZONE_P(timezone_object);
        switch (tzobj->type) {
            case TIMELIB_ZONETYPE_ID:
                tzi = tzobj->tzi.tz;
                break;
            case TIMELIB_ZONETYPE_OFFSET:
                new_offset = tzobj->tzi.utc_offset;
                break;
            case TIMELIB_ZONETYPE_ABBR:
                new_offset = tzobj->tzi.z.utc_offset;
                new_dst    = tzobj->tzi.z.dst;
                new_abbr   = timelib_strdup(tzobj->tzi.z.abbr);
                break;
        }
        type = tzobj->type;
    } else if (dateobj->time->tz_info) {
        tzi = dateobj->time->tz_info;
    } else {
        tzi = get_timezone_info();
        if (!tzi) {
            return 0;
        }
    }

    now = timelib_time_ctor();
    now->zone_type = type;
    switch (type) {
        case TIMELIB_ZONETYPE_ID:
            now->tz_info = tzi;
            break;
        case TIMELIB_ZONETYPE_OFFSET:
            now->z = new_offset;
            break;
        case TIMELIB_ZONETYPE_ABBR:
            now->z = new_offset;
            now->dst = new_dst;
            now->tz_abbr = new_abbr;
            break;
    }

timezone関連の処理が続いてますね。。。

    php_date_get_current_time_with_fraction(&sec, &usec);
    timelib_unixtime2local(now, (timelib_sll) sec);
    php_date_set_time_fraction(now, usec);

きた!

遂に見つけました...!!

php_date_get_current_time_with_fraction

正にというような名前ですね。

static void php_date_get_current_time_with_fraction(time_t *sec, suseconds_t *usec)
{
#if HAVE_GETTIMEOFDAY
    struct timeval tp = {0}; /* For setting microseconds */

    gettimeofday(&tp, NULL);
    *sec = tp.tv_sec;
    *usec = tp.tv_usec;
#else
    *sec = time(NULL);
    *usec = 0;
#endif
}

#if HAVE_GETTIMEOFDAYこの記述はC言語ifdef機能で、条件コンパイラのための機能です。 HAVE_GETTIMEOFDAY0 or 1で定義することで、どちらのコードが取り込まれるかが決まります。

C言語の現在時間取得メソッドである gettimeofday(&tp, NULL);time(NULL); 処理があります。

gettimeofday は POSIX に準拠した関数で、マイクロ秒単位の精度で現在の時刻を取得します。 linuxjm.osdn.jp

timeはC言語の時間ライブラリの関数です。 www.geeksforgeeks.org

ちなみにWindowsのようなOSの場合はどうなるのか??

windows環境ではHAVE_GETTIMEOFDAY1です github.com

windows環境でのgettimeofdayはここにある処理が走ります github.com

結果としてここのコードでwindows OSの時間を取得しています https://github.com/php/php-src/blob/5b01c4863fe9e4bc2702b2bbf66d292d23001a18/win32/time.c#L31

static zend_always_inline MyGetSystemTimeAsFileTime get_time_func(void)
{/*{{{*/
    MyGetSystemTimeAsFileTime timefunc = NULL;
    HMODULE hMod = GetModuleHandle("kernel32.dll");

    if (hMod) {
        /* Max possible resolution <1us, win8/server2012 */
        timefunc = (MyGetSystemTimeAsFileTime)GetProcAddress(hMod, "GetSystemTimePreciseAsFileTime");
    }

    if(!timefunc) {
        /* 100ns blocks since 01-Jan-1641 */
        timefunc = (MyGetSystemTimeAsFileTime) GetSystemTimeAsFileTime;
    }

    return timefunc;
}/*}}}*/

GetSystemTimePreciseAsFileTimeGetSystemTimeAsFileTimeという関数がwindowsの関数ですね。

Windows8以降でより高精度の時間を取得する関数が増えたために分岐があるようです。 metatrading.hatenablog.com

まとめ

ということで、PHPでCarbon::nowをコールするとPHP内部のC言語でOSのシステム時刻を取得しています!

Carbon::now(Carbonクラス) → DateTime(PHP) →gettimeofday/ time (C言語)→ OS ...

OSがどの様に時刻を刻んでいるか、その仕組みなど興味があれば色々調べてみると面白いです。 ja.wikipedia.org

何気なく使っている関数の裏側にはどんな処理が走っているか、WEB上や自分の環境で確認できる手段を持つことは理解を深める一手になるのではないかと思います。

【テックブログ】新卒エンジニアが入社2ヶ月で新規サービスをリリースした話。

こんにちは! 株式会社Hajimari22卒エンジニアの神野 凌太郎です。

普段は、事業部づけのエンジニアとして人事プロパートナーズの開発業務を担当しています。 内定者インターン〜新卒入社後の期間、人事プロパートナーズの開発と並行して 新規サービス「アミーチ」の開発・立ち上げを1人で行いました。

今回は、実際に新規事業の開発・立ち上げを行なって、学んだこと・反省点をまとめてお伝えできればと思います!

■新規サービス「アミーチ」概要

『人事専門型の求人サイト』として立ち上げたサービスです。 もともと、人事プロパートナーズ内で人材紹介を行なっており、 その専用サイトを立ち上げようというところからスタートしています。

人事を扱う求人サイトだからこそ、学校の保健室のように、 気軽にキャリアを考えられる、そんな求人サイトを作りたいという思いから、 「アミーチ」の立ち上げがスタートしました。

現在、リリースから約2ヶ月たち、 ユーザー数、求人数ともに右肩上がりで増えていますので、 正社員人事を採用したい企業様、人事職にチャレンジしたい、キャリアアップをしたいとお考えの求職者様、ぜひご登録をお待ちしております!

biz.ami-chi.com

ami-chi.com

■開発に着手するまで

開発をするにあたって、まずざっくりとした要件定義を行いました。 後述しますが、当時は何よりも早く開発することを優先していたため、 開発の段階では「ざっくりと要件を詰めれば良い」という認識でいました。

サイトに関しては、先に求人を募るために TO B → TO Cサイトの順でリリースを目指すことになりました。

具体的なスケジュールとしては、 TO B向けサイトは5月末、TO C向けサイトは6月中旬のリリースを目指しました。

技術選定は、Laravelとvue.jsを利用し、デザイン部分はbootstrapを利用することにしました。

サービスの立ち上げ・開発自体が初めての経験であったかつ、 1人で開発を行う(LPは外注しました)という状況だったので、 不安を抱えながら開発をすることになりました。

■開発過程〜リリースに至るまで

実際、TO B向けサイトは5月末、TO C向けサイトは6月中旬と スケジュール通りにリリースまで持っていくことができました。 しかし、リリースに至るまでいくつものハードルがありました。

要件が途中でいくつも変更があったこと

最も大変だったハードルは、「途中で要件が変わること」でした。

もともとフワッと始まったプロジェクトだったので、 要件をガチガチに決めていなかったこともあり、ビジネスサイドとの認識齟齬がある状態で 開発を進めてしまったためです。

特に、リリース直前にいくつか改善案や意見をもらったときは、 「本当にリリースできるのか?」と思ったくらい絶望的な状態でした。

ただ、もともとひいていたスケジュールより、 前倒しして開発を進めていたので、優先順位をつけて対応することで リリース時期がズレることなく、リリースすることができました。

学んだこととしては、

  • 時間がかかっても、要件定義はガチガチに固めてから開発をすること
  • キャパを越えそうなときは、優先順位を目に見える形で洗い出してから対応すること

です。

要件定義は、開発スピードを優先していたとしても、 きちんと行なったほうがスムーズに開発できることを身を持って知りました。(当たり前のことかもしれませんが) 最低でも、「テーブル定義」「機能設計」「画面設計」「URI設計」「モデル図」くらいは行なったほうが良いと思います。 アウトプットをあらかじめ行なっておくことで、プロジェクトメンバーとコンセンサスが取れている状態で認識齟齬なく開発を行うことができます。

メインサービス「人事プロ」との開発両立

2つ目は、「サービス開発の両立」です。 弊事業部では、エンジニア2人チームで行っているため、 新規サービスの開発だけに専念できるわけではありません。

別で、先輩エンジニアが大規模の新規サービス開発を行なっていたため、 そちらの開発にかかっきりな状態ということもあり、 メインサービスの「人事プロ」の開発業務も兼任していました。

新規サービスを立ち上げをするにあたって、 大前提メインの事業の収支を成り立たせないといけません。

また事業自体が少数精鋭で運営しているので、 エンジニア観点から常にインパクトのある改善、開発を行なっていく必要があります。

新規プロダクトの開発は楽しいし、やりがいもある一方で、 会社や事業部の誰かがメインの事業にコミットして数字を成り立たせてくれるからこそ、 チャレンジができます。

両立はすごく大変でしたが、改めて数字を成り立たせる重要性を身を持って体感しました。

不安との葛藤

1人で開発していたということもあり、常に不安との葛藤がありました。 特に、技術が成熟しているわけでもなく、「このコード設計で問題なく動くのかな…」といった不安は常につきまとっていました。

加えて、リソースもギリギリな状態で開発を行なっていたので、 納期へのプレッシャーもたくさんありました。

不安を解消するために行なったことは、 開発後の「セルフレビュー」と「テスト」です。

自分が書いたコードが動くわけがないという視点で セルフレビューとテストを行うことによって、サービスのクオリティを維持することができました。

学んだこととしては、開発をひと段落した後に安堵、過信をしないこと。 繰り返し確認・テストすることで、サービスクオリティ維持、不安の解消に繋がることを身を持って知ることができました。

■まとめ

新卒入社でいきなり新規サービスの開発を任される経験は滅多にないと思うので、 かなり良い経験でした!

実際に開発が終わり、リリースした時の感動は凄まじかった…!

まだサービスの改善をするところはたくさんあるので、 より良いサービスを目指して、日々開発に勤しもうと思います!

最後まで読んでいただきありがとうございました!


株式会社Hajimariでは、Laravelをメイン言語として自社開発・受託開発を行なっており、
一緒に開発を行なっていただけるエンジニア募集しています! 長野拠点の立ち上げメンバーも大募集しています!

興味のある方は以下の記事をぜひご覧ください!
みなさまとお会いできるのを心からお待ちしております!

www.wantedly.com www.wantedly.com www.wantedly.com

puppeteerでbootstrap導入時の影響調査をやってみた!

puppeteerを使ってみた!

こんにちは!
6月から23卒内定者インターン生として株式会社Hajimariに入社した、江端 凌です。

普段は、TUKURÜS事業部で受託開発の業務に携わっています。

今回は受託開発している既存サイトに、bootstrapを導入することになったので、どのような影響が出るのかを調査することになりました。

背景としては、既存サイトのCSSの複雑化と適切なブレークポイントの設置が出来ていなかったことから導入に至りました。

そこでbootstrapの導入前と導入後を比較するために、node.js製のライブラリであるpuppeteerを利用して全ページのスクリーンショットと差分画像を生成しました!

今回はpuppeteerの概要から、実装までの流れを紹介させていただきたいと思います!

puppeteerとは?

puppeteerとは、自動でブラウザ操作を行えるnode.jsのライブラリです。

npmでインストールすると、操作するchromiumも一緒についてきます。
puppeteerは、このchromiumでブラウザを操作していきます。

e-words.jp

puppeteerの概要としては、

  1. ヘッドレスで操作できる。
  2. 動的に生成されるページ(SPAなど)でも操作ができる。

といった特徴があります。

qiita.com

puppeteerを使ってやったこと

では、今回puppeteerを使ってやりたいことを確認していきます。

bootstrapを既存のサイトに導入した場合、すでに存在しているCSSと競合してデザイン崩れを起こす可能性があります。

その時、どのような影響が出るのかを調査したいため、

  • テスト環境(bootstrap未実装)
  • local環境(bootstrap導入済み)

の二つを利用して、puppeteerを使って両方の環境でスクリーンショットを撮りました。

その後、looks-sameというライブラリで差分画像を生成をします。

looks-sameもnode.jsのライブラリですので、puppeteerと一緒にインストールできますが、長くなるので今回は割愛させていただきます。

例として今回は、弊社のホームページのスクリーンショットを自動で撮影するプログラムを作りましょう!

puppeteerを導入してみる

では早速、npmを使ってpuppeteerをインストールしていきます!

node.jsをインストールをしていない場合は、node.jsのインストールを行なってください。

node.jsがインストールされているかどうか*1は、node -vコマンドで確認できます。

&#x60;node -v&#x60;コマンドの実行画面

qiita.com

npmでインストールする

まず、puppeteerを使うフォルダを作ります。

適当にデスクトップなど(どこでもいいです!)に移動して、以下のコマンドを実行してください。

$ mkdir puppeteer && cd puppeteer

そうしたらフォルダが作成されていると思います。

続いて、npmでpuppeteerをインストールしていきます!

$ npm init && npm install puppeteer

そうするとREPLで色々聞かれるので、全部Enterキーで答えましょう。

使うバージョンなどを聞かれています。

npm init &amp;&amp; npm install puppeteerの実行

画像のような表示になったらインストールは完了です。

puppeteer単体をインストールしたい方は、

$ npm install puppeteer-core

で単体でインストールすることが可能です。*2

実際にコードを書いてみる

今回は弊社のホームページスクリーンショットしてみます。

先ほど作成したpuppeteerフォルダ配下にindex.jsと画像を保存するdistフォルダを作成しましょう。

$ touch index.js && mkdir dist

このindex.jsの中に処理を書いていきます。

index.jsファイルがエントリーポイントになります。

e-words.jp

本当は全ページ撮影するなら、ConstsファイルにURLやログイン情報などを保存しておく方が便利ですが、今回は簡易的にindex.jsだけで書いていきます。

index.js

const puppeteer = require('puppeteer');

/**
 * 引数に渡した数 * ミリ秒待機
 * @param {int} msec
 * @returns setTimeout
 */
function sleep(msec) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve();
    }, msec);
  });
}

// puppeteerの設定
(async () => {
  const browser = await puppeteer.launch({
    // ヘッドレスかどうか
    headless: false,  
    // 10000ミリ秒応答が無かったら終了する
    timeout: 10000,  
  });

  const page = await browser.newPage();
  await page.setViewport({ width: 1920, height: 1080 });

  // ページに移動
  await page.goto('https://www.hajimari.inc/');

  // 画面遷移のアニメーションが終わるまで待機
  await sleep(8000);

  // スクリーンショットを取ってdistフォルダに保存する。
  await page.screenshot({ path: 'dist/hajimari.png', fullPage: false });

  // ブラウザを閉じる
  await browser.close();
})();

上記のコードを書いて保存したら、node index.jsをターミナルで実行しましょう。

$ node index.js

distフォルダ配下にhajimari.pngが保存されているはずです。

こんな感じで ↓

Hajimariのトップページ

それぞれのコードについて

それぞれのコードを見ていきましょう。

puppeteerは基本的に非同期で動作するので、asyncawaitで処理を書いていきます。

qiita.com

puppeteerの設定

(async () => {
  const browser = await puppeteer.launch({
    headless: false,  
    timeout: 10000,  
  });

  const page = await browser.newPage();
  await page.setViewport({ width: 1920, height: 1080 });

ここでは、puppeteerの設定ができます。

3行目のheadless: falseは、ヘッドレスにするかどうかを決めています。

今回はわかりやすく、falseにすることでchromiumの動きを見えるようにしました。必要なければtrueにすることで見えなくなります。

8行目で表示するサイズを変えられます。

await page.setViewport({ width: 1920, height: 1080 });

今回はPC表示にしていますが、スマホサイズで撮影したい場合は、
widthheightスマホのサイズにすることで撮影可能です。

スクリーンショットの処理

// ページに移動
await page.goto('https://www.hajimari.inc/');

// 画面遷移のアニメーションが終わるまで待機
await sleep(8000);

// スクリーンショットを取ってdistフォルダに保存する。
await page.screenshot({ path: 'dist/hajimari.png', fullPage: false });

// ブラウザを閉じる
await browser.close();

1行目で目的のページに移動します。

page.goto()に目的のURLを渡してあげましょう。ここを変更すると別のページにも飛ぶことができます。

2行目のsleep()は上の方で作った関数です。

function sleep(msec) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve();
    }, msec);
  });
}

細かい説明は省きますが、引数に渡した数字 × ミリ秒だけ待機します。

なので、await sleep(8000);は8秒待機させています。

3行目の処理はスクリーンショットをして、引数に渡した保存先に保存しています。

4行目でブラウザを閉じて、puppeteerの処理を終了しています。

まとめ

自分はこのpuppeteerアプリを作成したことで、何度も確認したりスマホ画面に切り替えたりできて効率化につながりました!

またヘッドレスでも動くので、puppeteerを動かしながら別の業務もできます。

今回は紹介しきれませんでしたが、差分画像を生成するlooks-sameというライブラリを使ったり、ベーシック認証を突破したり、Xpathで要素を取得してボタンを押したりもできたりして、とても幅広いです。

今後もE2Eテスト用に使ったりできそうなので、より良いものにできるように保守していきたいと思っています!


株式会社Hajimariでは、Laravelをメイン言語として自社開発・受託開発を行なっており、
一緒に開発を行なっていただけるエンジニア募集しています! 長野拠点の立ち上げメンバーも大募集しています!

興味のある方は以下の記事をぜひご覧ください!
みなさまとお会いできるのを心からお待ちしております!

www.wantedly.com www.wantedly.com www.wantedly.com

*1:puppeteer@3.0.0以降は、Node v10.18.1以降の環境が必要になるので、適宜アップデートしてください。

*2:puppeteer単体でインストールする場合は、chromiumはインストールされないので、注意してください。

PHPStan導入のすすめ

こんにちは!
株式会社Hajimari21卒エンジニアの古田 鏡です。

普段は、TUKURUS事業部(旧PIECE事業部)で
自社プロダクトであるスタートアップ向けマッチングサイト構築パッケージPIECE
https://crowd.itpropartners.com/piece/)の開発や受託開発を行っています!
※2022年4月から事業部の方針変更により事業名が変更となりました!

crowd.itpropartners.com

現在ジョインさせていただいている案件では、
組織のオンボーディングシステムの新規機能開発・システム統合等を担当させていただいております。

開発言語に関してはバックエンドでPHPフレームワークはLaravel)を使っていて、
静的解析ツールPHPStanを導入しているのですが、
このツールが便利だったので今回はご紹介していきたいと思います!

PHPの特徴

そもそもPHPは「動的型付け言語」に分類され、
基本的にはデータ型の宣言がいらないプログラミング言語です。

※参考 動的言語と静的言語の違い

型の宣言がいらないことで得られるメリットとしては、

  • どのような型の値でも代入でき、型(文字列・数値・配列・オブジェクト等)を意識する必要がない
  • (型の宣言がいらない分)記述量が少なくすむ
  • ソースコードの変更(修正)に強い

この辺が挙げられるのではないかと思います。

私もこれまで実際PHPの案件をやっている中で、
型を意識せずに面倒から解放される点でPHP最高ー!!と思っていました。

ですが、現在携わっている案件で、複数人での開発・システム統合をする中で、
コードの統合・不要コード削除に伴い、影響範囲の大きな修正が増えたことで、
静的解析の重要性を身に染みて感じました、、、
(修正がバグを生む可能性大、、、テストコードを全て書くのは時間がかかりますし、、、)

そこでPHPStanの出番です!!

●PHPStanとは?

PHPStan は、
PHP コードの静的解析ツールです。 (PHP Static Analysis Toolの略)
https://phpstan.org/

MIT ライセンスで公開されており、
Composer でインストールして利用できます。
また、Docker イメージも公式で公開されております。

github.com

phpstan.org

概要
  • 動作にはPHP 7.2以降が必要
  • 未定義の変数・メソッド・Class・プロパティなどを検出
  • PHPDocの構文チェックが可能

●ルールレベル設定

PHPStanはルールレベル設定があり、
レベルは0~9の10段階で設定できます!
レベル9ともなると結構厳しめのチェックとなるようです、、、!

phpstan.org

Lev 内容
0 基本的なチェック、未知のクラス、未知の関数、$this上で呼び出された未知のメソッド、それらのメソッドや関数に渡された引数の数が間違っている、常に未定義の変数をチェック
1 未定義の変数、call と get を持つクラスの未知のマジックメソッドとプロパティがある可能性がある
2 ($this だけでなく)すべての式で未知のメソッドをチェックし、PHPDocs を検証する
3 戻り値の型、プロパティに割り当てられた型の確認
4 基本的なデッドコードチェック - instanceofやその他の型チェックが常にfalse、到達しないelse文、return後の到達不能コードなど
5 メソッドや関数に渡される引数の型チェック
6 タイプヒントの欠落を報告する
7 部分的に間違っている論理和型の報告 - 論理和型の一部の型にしか存在しないメソッドを呼び出した場合、レベル7はそのことを報告し始めます(その他の不正確な状況も)
8 null可能な型に対するメソッド呼び出しとプロパティへのアクセスを報告する
9 混合型に厳密であること - この型で唯一許される操作は、この型を別の混合型に渡すことである

下記に例題が載っているのでチェックしてみてください!

phpstan.org

●インストール方法

インストールする方法としては

phpstan.org

公式にインストールする方法が記載してあるので、それに従いインストールします。

Composerを使い下記のコマンドを実行します。

composer require --dev phpstan/phpstan

基本的に開発環境で使用するものだと思いますので、
--devオプションをつけるのが良いと思います。

インストールできているか確認

./vendor/bin/phpstan analyse --version
PHPStan - PHP Static Analysis Tool 0.12.99⇦インストールされているバージョンを表示

●実行方法

実行コマンド
./vendor/bin/phpstan analyse

ルールレベルを変更する場合には、 レベルを変更するには --level (-l )オプションで実行します。
↓ではレベル4で設定しています。

.\vendor\bin\phpstan analyse -l 4
設定ファイル(phpstan.neon 一部抜粋)
includes:
    - ./vendor/nunomaduro/larastan/extension.neon

parameters:

    paths:
        - app

    level: 4
実行結果

エラーありの場合

エラーなしの場合

こんな感じで結果が出ます。

●実際に使ってみて得られたこと

  • 導入のしやすさ
    基本的に公式通りでインストールでき、導入のハードルが低いこと。

  • 解析が高速にできる
    状況に応じて、ルールレベル・実行範囲を変えられ、必要部分に対して解析が高速にできること

  • レビューの負担軽減
    システム統合など大きな改修を行う場合、コードレビューの量が多くなりがちですが、
    事前に静的解析を行うことでレビューの量を減らせること
    (個人的にはこの効果が一番大きいと思いました。)

  • ある程度システムの安全性・整合性を担保できる
    型レベルの安全性・整合性が保たれること
    しかしながら、無限ループや関数の型がmixed・null許容の場合、解析をすり抜けてしまうことがあること

●まとめ

大前提として、別途ユニットテスト等は必要だと思いますが
導入と実行の容易さ、時間短縮の観点から非常にコスパの良いツールだと思いました!
プロジェクトが大きくなればなるほど、効果を発揮するツールだと思いますので、是非導入してみてはいかがでしょうか!


株式会社Hajimariでは、Laravelをメイン言語として自社開発・受託開発を行なっており、
一緒に開発を行なっていただけるエンジニア募集しています! 長野拠点の立ち上げメンバーも大募集しています!

興味のある方は以下の記事をぜひご覧ください!
みなさまとお会いできるのを心からお待ちしております!

www.wantedly.com www.wantedly.com www.wantedly.com

Notionで新卒採用管理システムを作ってみた!

こんにちは!
4月に入社した、株式会社Hajimari22卒エンジニアの神野 凌太郎です。

普段は、人事プロパートナーズの開発業務や事業部内でリリースする新規サービスの開発業務を担当しています。

本業はエンジニアですが、内定者時代から23卒エンジニア採用の兼務をしていて、
約1年前、23卒採用が本格的に始まる前にNotionで新卒採用管理システムを作成しました。

今回は、実際に1年間運用してみてよかったことや、改善できたなと思うことを まとめてお伝えできればと思います!

■そもそもNotionとは

www.notion.so

Notionは、ドキュメントやデータベース、TODO管理など
あらゆる情報やデータを1つに集約して管理できるSaaS型のツールです。

弊社では、Notionを社内wikiとして全社で導入しています。
他にも各事業部で、チームのタスク管理やTODO管理で活用していたり、
採用募集のページを公開したりしています。

また約半年前に、弊社のイベントスペースで
NotionTokyoのオフラインイベントが行われたりもしました!

https://notiontokyo14.splashthat.com

■Notionを導入した経緯

Notionシステムを導入する前は、 Slackでチャンネルを作成し、そのチャンネルで候補者ごとにスレッドを立てて申し送りや引き継ぎが行われていました。

課題

Slackを使って申し送りを行うことで、スピーディーに情報共有ができる一方で、 下記の問題点がありました。

  • 候補者の情報を見るために、過去のスレッドを辿る必要がある(ピーク時にはすぐに上に流れていってしまうので、スレッドを見つけるのは困難です)
  • ステータス管理を別でスプシで行っていたが、 更新漏れがたびたび発生しており、リアルタイムでのステータス管理ができていなかった
  • 情報が点在しているため、データ分析をすることが難しく定性的な判断で採用活動を行なっていた

実装する手間を最小限にリアルタイムにデータを集約しようということで、 Notionで新卒採用管理システムを作成することにしました。

■全体の流れ

Notionを使った一連のフローを下記の図にまとめました。
Notionを使うのは、23卒採用メンバー + 面接を担当する社員です。

初回接触の場合は、学生テーブルにレコードを追加し、
基本情報(名前や次回面接日)や関連情報(大学、応募経由など)を記入します。

面接後は評価や所感を記入し、社内の専用Slackに情報共有、エージェントや学生に結果連絡を行います。

2回目以降の面接に関しては、基本的に23卒採用メンバー以外の社員が担当します。
担当社員は、面接終了後にNotionのステータスの更新、結果・所感の記入を行い、
リクルーター(該当学生を担当している23卒採用メンバー)にSlackで連絡を行います。

Notionを活用したときの一連のフロー

■テーブル構成

テーブル構成は以下の通りです。 学生テーブルを中心として、それぞれ「採用メンバー」「大学」「エージェント」「リクルーター」のテーブルと連携しています。「リクルーター」は23卒採用メンバーを指し、 「採用メンバー」は2次面接以降を担当する社員を指します。

Notionのリレーション機能を使った実装を行い、リレーションを利用したのはそれぞれのテーブル別でデータを蓄積するためです。

テーブル構成

■作成にあたって意識したこと

大前提として、23卒採用メンバーにNotionを活用してもらわなければ意味がありません。 導入目的は、リアルタイムにステータス、情報更新してもらうことだからです。

ゴールは、メンバーに「使ってもらえるかどうか」。 そのため、多少データベースの構成が複雑になっても、UIを優先しました。

トップページの親しみ

メンバーはかなりの頻度でNotionを開くので、トップページをこだわろうと思いました。 特にカバー画像は目に優しく、可愛くしようと思っていた時に、たまたま近くに同期の女の子がいたので、作成を依頼しました。めちゃくちゃ可愛い!

トップページ

使い方ページの作成

当時、全社でNotionが導入されたばかりだったこともあり、 Notionの操作に慣れないメンバーがほとんどでした。

また、Notionを使うのは23卒採用メンバーだけではなかったので、
多くの人に等しく操作に慣れてもらう必要がありました。
そこで、使い方ページの作成を行いました。

  • 文字だけではなく動画を使って説明すること
  • リクルーターと面接担当者別のフローに沿って、必要な情報だけ提示すること

意識したことは上記の2つです。
使い方ページを導入したおかげで、
導入障壁があまりない状態で、運用をスタートさせることができました。

記入コストを下げること

冒頭でも記述した通り、ゴールは使ってもらえるかどうかです。
そのため、記入するコストを下げることを意識しました。
具体的には、考えなくても直感的に記述できること。

まず、Notionのテンプレート機能を利用し、
極力労力をかけないで良いように、学生ページのテンプレートを用意しました。

学生ページのテンプレート利用方法
基本情報は、一覧画面で情報の記入や更新ができるようにし、
詳細な情報(学生情報、面談メモ)は学生ページで記述できるようにしました。

見やすさも意識しつつ、記入に思考体力を奪わないように設計を心がけました。

■問題点

実際に1年間運用してみた結果、いくつか問題点が出てきました。

ページが重く、表示速度が遅い

最もクリティカルだったのは、ページが重く、表示速度が遅いことです。
原因は、学生レコードの急激な増加リレーションを貼りすぎたことだと推測できます。

年度の途中で採用予定人数が大幅に増えたことも相まって
学生レコードが増加し、現在700レコード近くデータが入っています。

その上、そのレコードに各関連テーブルのリレーションが貼られている状態なので、
さらにページを重くしています。

学生ページ内、面談評価の面談担当者と採用メンバーテーブルがリレーションを貼っており、特に最も重くしている原因として考えられます。
学生テーブルの学生ページ内でリレーションを貼っているので、採用メンバー全員が面談担当者と紐付いた状態になっているからです。
(しかも、採用メンバーテーブルであまりデータを貯める必要がないことも後からわかったので、リレーションを貼る必要性もありません。)

学生ページ内の面談評価テーブル
この後に、シンプルテーブルの機能がリリースされました。
採用メンバーのテーブルはこれがベストだと感じました。(zoomのリンクやWantedlyの記事の管理は行いたいからです)

www.notion.so

データ集計が難しい

残念ながら、Notionにはスプレッドシートやエクセルのように便利な関数がいくつも用意されていません。
当初はNotion内でデータ分析がリアルタイムに見れる状態を作ろうと思い、
用意されている関数を駆使して、無理やり集計ができるようにしましたが、
あまり活用できていません。

データ分析自体は、スプレッドシートをNotionに埋め込んで同期できるように運用した方が良さそうです。

■まとめ

採用管理システムをNotionで作成したことによって、 データを一元に管理できるようになったので非常に良かったです!
実際に運用してみて問題点が浮き彫りになったので、 24卒以降の管理システムは、より良いシステム構築ができれば良いなと思っています。
1年前に作成した時よりも、機能がかなり充実しているので、 うまく活用していきたいと思います。

Notion大好き!


株式会社Hajimariでは、Laravelをメイン言語として自社開発・受託開発を行なっており、
一緒に開発を行なっていただけるエンジニア募集しています! 長野拠点の立ち上げメンバーも大募集しています!

興味のある方は以下の記事をぜひご覧ください!
みなさまとお会いできるのを心からお待ちしております!

www.wantedly.com www.wantedly.com www.wantedly.com

22卒内定者が『フリーランス人事と案件のマッチングスコア算出』機能をリリースしました!

皆さん初めまして、
22卒内定者で人事プロパートナーズで開発を担当している、神野凌太郎と申します!

今回、人事プロパートナーズ内でフリーランス人事と案件のマッチングスコア算出」機能をリリースしたので、リリースに至った経緯と機能詳細についてご紹介したいと思います!

■人事プロサービス概要

f:id:r_kamino:20211122124847p:plain

各社が抱える組織課題の解決に強みを持った即戦力人事の業務委託人材と
人事のリソース不足でお困りの成長企業をマッチングするサービスです。

興味のある方は、
ホームページからぜひお問い合わせください!

itpropartners.com

■機能開発に至った背景

弊サービスでは、案件をご紹介する前に専属CA(キャリアーエージェント)がご登録いただいたフリーランス人事と面談を行います。

面談では、

  • どういう経験をされてきたか
  • 案件にはどのくらいの日数や、週何時間くらい稼働できるか

などをお聞きして、管理画面に面談情報を登録していきます。

その後、登録した情報をもとにフリーランス人事に案件をご紹介しています。
ただ、弊サービスには多くのフリーランス人事の方にご登録いただいているため、企業担当が案件にマッチするフリーランス人事をピックアップするのが困難です。
そのため、企業担当からCAに案件概要を説明した後、
CAが案件にマッチするフリーランス人事ピックアップしています。

f:id:r_kamino:20211122124958p:plain

問題点としては、

  • 企業担当とCAのコミュニケーションコストが大きいこと
  • CAの属人的な情報で、案件とフリーランス人材のマッチングを行なっていること

が挙げられます。

そこで、「人と案件のマッチングスコア算出」機能をリリースすることで、
少しでも企業担当とCAの工数を削減し、より最適なマッチングを実現しようと考えたわけです。

■マッチングスコア算出のロジック

マッチングスコアは、以下の8項目それぞれの入力内容を突き合わせて算出します。
具体的には、それぞれの項目で0~1の間で評価を行い、設定した重みをかけ合わせて
スコアを算出しています。

f:id:r_kamino:20211122130155p:plain

企業担当とCAのヒアリング結果から、
「月間稼働時間」「勤務形態」「タグ」「稼働可能時間帯」がマッチングにおいて特に大事な項目だということがわかったので、他の項目に比べて重みが大きくなっています。

また、案件・フリーランス人事のどちらか未入力の項目があった場合は、
自動的にその項目は0として評価をします。
項目によっては未入力のほうが入力していた時よりもスコアが大きくなる場合があるからです。

 ■実装手順

 ①仮の評価方法を作成

まず、フリーランス人事と案件の入力項目を見比べて、マッチングに必要な項目を洗い出しました。
項目を洗い出すなかで、どちらか一方にしか項目でも、マッチングに使える項目と判断したものに関しては、新たに項目を追加する想定でピックアップをしました。
ここで、既存のデータにこだわるのではなく、あくまでマッチングに必要な要素は何かという判断軸でピックアップを行うことを意識しました。

②過去データをもとに、手動で評価値を算出

過去のフリーランス人事とその方が稼働している案件情報を用いて、評価値を算出しました。
実際にマッチングが成立した情報を使った時に、評価値に問題がないかを確認するためです。

結果として、全ての項目を0、1だけで評価してしまうと、明らかに他の項目より評価が高くなってしまう項目があったので、それぞれの項目に重みをつける方法を取ることにしました。
データベースに重みの値を保存できるようにして、いつでも値を変えられるようにします。

③CAと企業担当にヒアリング

②までに作成した叩きを用いて、CAと企業担当と要件のアップデートを行いました。
マッチングに用いる項目と重みに問題がないかを確認してもらい、評価方法の改善をするのが目的です。

ヒアリングにおいてCAと企業担当どちらかに偏った意思決定にならないように、
中立に意見を取りまとめるように心がけました。

④入力フォームの設置

要件のアップデートで新たに出てきたマッチング項目を管理画面で登録できるように、入力フォームの設置を行いました。(システム設計上、複数のデータベースと入力ページの改修が必要で、想定以上の工数がかかりかなり大変でした。。)

⑤自動マッチングの実装

マッチングに必要な項目を入力できるようにしたので、最後に自動マッチングの実装に取り掛かりました。
アップデートした要件をもとに、それぞれの項目でスコア算出のコードを書きました。(全部で200行くらいのボリュームです、、)

また、算出したスコアを用いて「フリーランス人事→案件」「案件→フリーランス人事」でマッチング検索できるページを実装しました。

下記の図は、「案件→フリーランス人事」で検索したページの例です。
算出したスコアの高い順でユーザー表示を行うことで、フリーランス人事と案件の最適なマッチングを提案します。

 

f:id:r_kamino:20211122130552p:plain

■開発を通して大変だったこと

最も大変だったことは、自動マッチングの抽象的な要件を具体に落とし込む作業です。

もともと、「フリーランス人事と案件を自動マッチングできたら良いよね」と開発チームでの何気ない会話からスタートしているので、「やりたいことは決まっているけど、具体的には何も決まっていない」状態でした。

そのため、具体的な実装要件を詰める前に、ビジネスロジックをプログラムに置き換える作業が必要です。

まず、スラック上で企業担当とCAがやり取りしているテキストからマッチングに必要な項目を推測し、開発チームで叩きを作成することから始めました。(叩きを作成するのに半日以上かかりました、、)

「叩きがある状態だと効率的に要件を詰めることができる」と上長から教えてもらったという背景から、叩きを先に作成することにしました。

実際に、叩きがある状態で議論をすると、より本質的なマッチング要件を決めることができ、
実装もスムーズに行うことができました。

学んだこととしては、「実装前の要件整理に時間をかけること。」
時間をかけた分だけ、ビジネスに寄り添った機能開発ができることを実感しました。

■まとめ

実際にこの機能をリリースすることで、

  • 月間12.5時間の削減
  • 客観的な情報で誰でも人選が可能な状態

を実現することができました。

実際にCAと企業担当からは、「最適なマッチングが実現できて、リソースが余った分、さらに別のところで価値提供が行えるようになった」というお声をいただきました!

今後、この機能を活用してフリーランス人事の方に直接案件をレコメンドできる機能のリリースも考えています。

他にもまだ活かしきれていないデータは数多くあるので、うまくデータ活用を行なってサービス向上に寄与していきたいと思います!

最後までお読みいただきありがとうございました!

------------------------------------------------------------------------------------

株式会社Hajimariでは、Laravelをメイン言語として自社開発・受託開発を行なっており、一緒に開発を行なっていただけるエンジニア募集しています!

23新卒エンジニアの募集も開始しておりますので、少しでも興味ある方はカジュアルにお話ししましょう!

興味のある方は以下の記事をぜひご覧ください!
みなさまとお会いできるのを心からお待ちしております!

www.wantedly.com

www.wantedly.com

www.wantedly.com

www.wantedly.com

www.wantedly.com

www.wantedly.com

www.wantedly.com

www.wantedly.com

一泊二日開発合宿!〜思わず移住したくなる森のオフィス〜

こんにちは!

株式会社Hajimari21卒内定者エンジニアの古田です。

今年の6月より内定者インターンとしてHajimariで働いております!

 

普段は、企業と優秀な人事のマッチングサービス

人事プロパートナーズ」の開発業務を行っております!

 

毎日学ぶ事だらけですが、会社の良いメンバーにも恵まれ、

非常に充実した日々を送っております!!!

やっぱり、仕事をする上で誰と働くかが非常に重要だな

最近は、そんなことを実感しております!


さて、今回は社内のエンジニアメンバーと共に一泊二日で開発合宿に行ってきました!

その様子をこのブログで紹介させていただきます!

 

 

開発合宿を行った「富士見 森のオフィス」 

f:id:kyoK:20201109144216p:plain

f:id:kyoK:20201111121752p:plain

(森のオフィス ホームページより)

今回、開発合宿は長野県にある

富士見 森のオフィス」という施設に行ってきました!

 

大自然の中にあるこの施設は、コワーキングスペース宿泊棟が併設されており、

普段はサテライトオフィステレワーク拠点

地域住民の方々にとっての“公民館”的スペースとして使われている施設で、

Hajimariのオフィスがある渋谷からは車で2時間半程のところにあります!

 

 

 

 

コワーキングスペースの様子

f:id:kyoK:20201109145440j:plain

f:id:kyoK:20201109145358j:plain

 

施設内はとても静かで、ストレスを微塵も感じさせない

コワーキングスペースでの作業はめちゃめちゃ集中できました!

少し疲れてきた時にパソコンから目を逸らし、

窓の外に目を向けると辺りには木々が広がっていて癒されました!

開放感が半端なかったです!!

 

施設の外のベンチで作業をしているメンバーもいましたが、

周りに木々の音しか聞こえない中での作業は

都心で働いていては味わう事ができないですし、

そんなストレスフリーな環境がこの施設の一番の魅力だと思いました!

 

 

施設周辺の様子 

f:id:kyoK:20201109144914j:plain

森のオフィスというだけあって、辺りは木々に囲まれていました!

紅葉シーズンだった事もあり、とても綺麗で癒されました!

(僕の語彙力では表現し切れない事が非常に悔しいです、、、)

 

昼食

f:id:kyoK:20201111114316j:plain

f:id:kyoK:20201111114337j:plain

昼食はコワーキングスペースの外にキッチンカーが来ており、

僕はココナッツチキンカレーを頼みました!

美味しかったです、そして健康的!!!

食べ終わった後、

施設内のキッチンで自分の使った食器を洗うと50円引きしてくれます!!!

 

食器洗いをしている時に、

〜日より移住して来ました」という声がちらほら聞こえていたのですが、

これだけ居心地の良い場所なら移住したくなる気持ちが分かるなと思いました。

 

宿泊施設の様子

f:id:kyoK:20201109144846j:plain

f:id:kyoK:20201109163629p:plain

(森のオフィス ホームページより)

 

 

コワーキングスペースの隣にある宿泊施設はとても綺麗でした!!!

今回は、偶然にも他の団体のお客さんがいなかったので貸し切り状態でした!

将来はこんな家に住みたい、、、思わずそう思ってしまいました!

 

f:id:kyoK:20201109163046j:plain

 

夕食は施設の近くにある

生産者直売所の「たてしな自由農村」で

猪や鹿のジビエ、野菜、地酒などを購入し、

宿泊施設のキッチンで調理して食べました!!

 

f:id:kyoK:20201109161807j:plain

特にジビエが最高でした!!!

f:id:kyoK:20201109163033j:plain

 

美味しいご飯を食べながら、

普段はしないような話をできましたし、

よりコミュニケーションが深まりました!!

これもまた、開発合宿の良さなのではないかと思います!

 

 

途中から登山合宿に・・・ 

初日にコワーキングスペースの管理者の方から、

今の時期(11月初旬)は宿泊施設の近くにある

富士見パノラマリゾートという施設のゴンドラで山を登ると

日の出と共に雲海見えるよ!というお話を聞いたので、

メンバー皆で早朝4時に起きて車でゴンドラに乗りに行きました!

 

ところが、、、

 

その日に限って、ゴンドラは営業しておらず雲海を見ることはできませんでした、、

(雲海みたかった、、、、)

実際の雲海はこんな感じだそうです。

 

https://www.fujimipanorama.com/

 

せっかく早起きしたのに、このまま帰るのも勿体なかったので、

せめて高台で日の出でも見ようか!という話になり、

歩いて山道を登り、高台を目指しました!

 

f:id:kyoK:20201109180008j:plain

 

少し歩いたら高台に着くだろうと安易な考えで登り始めましたが、

そんなに甘くはありませんでした、、

 

予定より時間がかかった事もあり、

登っている途中で日の出を迎えることになりました。

完全に予定外でしたが、これはこれで綺麗でした!!

 

f:id:kyoK:20201109181538j:plain

 

結果的に、山道を往復で2時間程歩いてきました!

良い運動になったと言えばなりましたが、

流石に少しだけ疲れましたね、、、

 

 

最後に

2日間通して、普段では体験できない事だらけの開発合宿になりました!!

今回合宿に参加したメンバーの全員、

開発合宿に行ってよかったと思っていましたし、

もし次回行く事があれば、次は場所をどこにしようか!

次の合宿までにサービス改善案を出して、合宿内で開発しきろう!

などと行った案が出ています!

 

ストレスフリーで作業効率がめちゃめちゃ上がりメンバーとも仲が深まる!

開発合宿は良い事づくめです!

普段、都内で開発業務に勤しんでいる方、

ストレスフリーで開発をしたい方、

今まで以上にメンバーとの仲を深めたい方は

是非、開発合宿に行ってみてはいかがでしょうか?