0
1

More than 1 year has passed since last update.

OSSソースコードリーディング(stefanzweifel/screeenly)

Posted at

1. はじめに

今回コードリーディングしたものは下記のOSSのコード。
stefanzweifel/screeenly

  • スクリーンショットのAPIを提供するサービス
  • starは400程
  • PHP(Laravel)で記述されている
  • 参考になったコードとコードの下にコメントを記載

2.本文

Interface

modules/Screeenly/Contracts/CanCaptureScreenshot.php
<?php

namespace Screeenly\Contracts;

use Screeenly\Entities\Url;

interface CanCaptureScreenshot
{
    public function capture(Url $url, $storageUrl);
}
  • Screeenly\Contracts配下にinterfaceを格納
  • 関数はcaptureのみ
  • 返り値はここでは定義されていないがScreeenly\Entities\Screenshotが返却される

modules/Screeenly/Services/ChromeBrowser.php
 <?php

namespace Screeenly\Services;

use Illuminate\Support\Facades\Storage;
use Screeenly\Contracts\CanCaptureScreenshot;
use Screeenly\Entities\Screenshot;
use Screeenly\Entities\Url;
use Spatie\Browsershot\Browsershot;

class ChromeBrowser extends Browser implements CanCaptureScreenshot
{
    public function capture(Url $url, $filename)
    {
        $browser = Browsershot::url($url->getUrl())
            ->ignoreHttpsErrors()
            ->windowSize($this->width, is_null($this->height) ? 768 : $this->height)
            ->timeout(30)
            ->setDelay($this->delay * 1000)
            ->userAgent('screeenly-bot 2.0');


        if (config('screeenly.disable_sandbox')) {
            $browser->noSandbox();
        }

        if (is_null($this->height)) {
            $browser->fullPage();
        }

        Storage::disk(config('screeenly.filesystem_disk'))->put($filename, $browser->screenshot());

        $path = Storage::disk(config('screeenly.filesystem_disk'))->path($filename);

        return new Screenshot($path);
    }
}
  • CanCaptureScreenshot(interface)の実装先
  • ChromeBrowserは外部パッケージの「Spatie\Browsershot」を使用 * github

modules/Screeenly/Services/AwsBrowser.php
<?php

namespace Screeenly\Services;

use Illuminate\Support\Facades\Storage;
use Screeenly\Contracts\CanCaptureScreenshot;
use Screeenly\Entities\Screenshot;
use Screeenly\Entities\Url;
use Wnx\SidecarBrowsershot\BrowsershotLambda;

class AwsBrowser extends Browser implements CanCaptureScreenshot
{
    public function capture(Url $url, $filename)
    {
        $browser = BrowsershotLambda::url($url->getUrl())
            ->ignoreHttpsErrors()
            ->windowSize($this->width, is_null($this->height) ? 768 : $this->height)
            ->timeout(30)
            ->setDelay($this->delay * 1000)
            ->userAgent('screeenly-bot 2.0');


        if (config('screeenly.disable_sandbox')) {
            $browser->noSandbox();
        }

        if (is_null($this->height)) {
            $browser->fullPage();
        }

        Storage::disk(config('screeenly.filesystem_disk'))->put($filename, $browser->screenshot());

        $path = Storage::disk(config('screeenly.filesystem_disk'))->path($filename);

        return new Screenshot($path);
    }
}

modules/Screeenly/Providers/ScreeenlyServiceProvider.php
<?php

namespace Screeenly\Providers;

use Illuminate\Support\ServiceProvider;
use Screeenly\Contracts\CanCaptureScreenshot;
use Screeenly\Guards\ScreeenlyTokenGuard;
use Screeenly\Services\AwsBrowser;
use Screeenly\Services\ChromeBrowser;

class ScreeenlyServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {
        $this->app['view']->addNamespace('screeenly', base_path().'/modules/Screeenly/Resources/views');

        if (config('screeenly.use_aws_lambda_browser') === true) {
            $this->app->bind(CanCaptureScreenshot::class, AwsBrowser::class);
        } else {
            $this->app->bind(CanCaptureScreenshot::class, ChromeBrowser::class);
        }

        auth()->extend('screeenly-token', function ($app, $name, array $config) {
            return new ScreeenlyTokenGuard(
                auth()->createUserProvider($config['provider']),
                $this->app['request']
            );
        });
    }

    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}
  • ServiceProviderを継承したScreeenlyServiceProviderクラスにてinterfaceをbind
  • configのuse_aws_lambda_browserの値がtrueであればCanCaptureScreenshot::classをAwsBrowser::classに注入。use_aws_lambda_browserがfalseであれば、CanCaptureScreenshot::classを ChromeBrowser::classに注入

config/screeenly.php
<?php

return [

    /*
     * Disable Chrome Sandbox
     * See https://github.com/stefanzweifel/screeenly/issues/174#issuecomment-423438612
     */
    'disable_sandbox' => env('SCREEENLY_DISABLE_SANDBOX', false),

    /**
     * The Filesystem disk where screenshots are being stored
     */
    'filesystem_disk' => env('SCREEENLY_DISK', 'public'),


    'use_aws_lambda_browser' => env('SCREEENLY_USE_AWS_LAMBDA_BROWSER', false),
];
  • config/screeenly.phpの中身
  • 環境変数「SCREEENLY_USE_AWS_LAMBDA_BROWSER」はデフォルトではfalseが格納されている。よってCanCaptureScreenshotはデフォルトではChromeBrowserに注入される

modules/Screeenly/Services/CaptureService.php
<?php

namespace Screeenly\Services;

use Illuminate\Support\Str;
use Screeenly\Contracts\CanCaptureScreenshot;
use Screeenly\Entities\Url;

class CaptureService
{
    /**
     * @var Screeenly\Entities\Url
     */
    protected $url;

    /**
     * @var Screeenly\Services\Browser
     */
    protected $browser;

    public function __construct(CanCaptureScreenshot $browser)
    {
        $this->browser = $browser;
    }

    /**
     * Set Height.
     * @param  int $height
     * @return Screeenly\Services\CaptureService
     */
    public function height($height)
    {
        $this->browser->height($height);

        return $this;
    }

    /**
     * Set Width, defaults to 100%.
     * @param  int $width
     * @return Screeenly\Services\CaptureService
     */
    public function width($width)
    {
        $this->browser->width($width);

        return $this;
    }

    /**
     * Set Delay in milliseconds, defaults to 1000.
     * @param  int $delay
     * @return Screeenly\Services\CaptureService
     */
    public function delay($delay)
    {
        $this->browser->delay($delay);

        return $this;
    }

    /**
     * Set Url to capture.
     * @param  Screeenly\Models\Url    $url
     * @return Screeenly\Services\CaptureService
     */
    public function url(Url $url)
    {
        $this->url = $url;

        return $this;
    }

    /**
     * Trigger capture action.
     * @return Screeenly\Entities\Screenshot
     */
    public function capture()
    {
        $filename = uniqid().'_'.Str::random(30) . ".png";

        return $this->browser->capture(
            $this->url,
            $filename
        );
    }
}
  • Contracts\CanCaptureScreenshotのクライアント
  • CaptureService.phpはContracts\CanCaptureScreenshotの実装先を知らない(具象ではなく抽象に依存)
  • 依存関係逆転の法則(DIP)

modules/Screeenly/Services/Browser.php
<?php

namespace Screeenly\Services;

use Exception;

class Browser
{
    /**
     * @var int
     */
    public $height = null;

    /**
     * @var int
     */
    public $width = 1024;

    /**
     * @var int
     */
    public $delay = 1;

    /**
     * Set Height.
     * @param  int $height
     * @return Screeenly\Services\Browser
     */
    public function height($height = 100)
    {
        $this->height = $height;

        return $this;
    }

    /**
     * Set Width, defaults to 100%.
     * @param  int $width
     * @return Screeenly\Services\Browser
     */
    public function width($width = 100)
    {
        if ($width > 5000) {
            throw new Exception('Screenshot width can not exceed 5000 Pixels');
        }
        $this->width = $width;

        return $this;
    }

    /**
     * Set Delay in miliseconds, defaults to 1000.
     * @param  int $delay
     * @return Screeenly\Services\Browser
     */
    public function delay($delay = 1000)
    {
        if ($delay > 15000) {
            throw new Exception('Delay can not exceed 15 seconds / 15000 miliseconds');
        }

        $this->delay = $delay;

        return $this;
    }
}
  • AwsBrowser, ChromeBrowserの親クラス
  • AwsBrowser, ChromeBrowserで使用される、共通メソッドを定義
  • 定義されているメソッドの内容はsetter、値のバリーデーションのみ

tests/modules/Screeenly/integration/api/v2/ApiV2ScreenshotTest.php
<?php

use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Storage;
use Screeenly\Contracts\CanCaptureScreenshot;
use Screeenly\Models\ApiKey;
use Screeenly\Services\InMemoryBrowser;

class ApiV2ScreenshotTest extends BrowserKitTestCase
{
    use DatabaseTransactions;
    use InteractsWithBrowser;

    /** @test */
    public function it_shows_error_message_if_no_api_key_is_provided()
    {
        $this->json('POST', '/api/v2/screenshot', [])
            ->seeJson([
                'error' => 'Unauthenticated.',
            ]);
    }

    /** @test */
    public function it_shows_error_if_no_url_is_provied()
    {
        $apiKey = ApiKey::factory()->create();

        $this->json('POST', '/api/v2/screenshot', [
            'key' => $apiKey->key,
        ])
            ->seeJson([
                'url' => ['The url field is required.'],
            ]);
    }

    /** @test */
    public function it_shows_error_if_not_a_url_is_passed()
    {
        $apiKey = ApiKey::factory()->create();

        $this->json('POST', '/api/v2/screenshot', [
            'key' => $apiKey->key,
            'url' => 'Foo',
        ])
            ->seeJson([
                'url' => ['The url format is invalid.', 'The url is not a valid URL.'],
            ]);
    }

    /** @test */
    public function it_returns_base64_representation_of_screenshot()
    {
        Storage::fake(config('screeenly.filesystem_disk'));

        Storage::disk(config('screeenly.filesystem_disk'))
            ->put(
                'test-screenshot.jpg',
                file_get_contents(storage_path('testing/test-screenshot.jpg'))
            );

        $apiKey = ApiKey::factory()->create();

        $this->app->bind(CanCaptureScreenshot::class, function ($app) {
            return new InMemoryBrowser('http://foo.bar', '/path/to/storage');
        });

        $this->json('POST', '/api/v2/screenshot', [
            'key' => $apiKey->key,
            'url' => 'http://google.com',
        ])
        ->seeJsonStructure([
            'data' => [
                'path', 'base64',
            ],
        ]);
    }
}
  • テストではInMemoryBrowserがバインドされる
modules/Screeenly/Services/InMemoryBrowser.php
<?php

namespace Screeenly\Services;

use Screeenly\Contracts\CanCaptureScreenshot;
use Screeenly\Entities\Screenshot;
use Screeenly\Entities\Url;

class InMemoryBrowser extends Browser implements CanCaptureScreenshot
{
    /**
     * Capture Url and store image in given Path.
     * @param  Url    $url
     * @param  string $storageUrl
     * @return Screeenly\Entities\Screenshot
     */
    public function capture(Url $url, $storageUrl)
    {
        return new Screenshot(storage_path('testing/test-screenshot.jpg'));
    }
}

依存関係
altテキスト

Service

ScreenshotController
<?php

namespace Screeenly\Http\Controllers\Api\v2;

use App\Http\Controllers\Controller;
use Screeenly\Entities\Url;
use Screeenly\Http\Requests\CreateScreenshotRequest;
use Screeenly\Models\ApiKey;
use Screeenly\Services\CaptureService;

class ScreenshotController extends Controller
{
    /**
     * @var Screeenly\Services\CaptureService
     */
    protected $captureService;

    public function __construct(CaptureService $captureService)
    {
        $this->captureService = $captureService;
    }

    /**
     * Create a new Screenshot.
     * @param  CreateScreenshotRequest $request
     * @return Illuminate\Http\Response
     */
    public function store(CreateScreenshotRequest $request)
    {
        $apiKey = ApiKey::where('key', $request->key)->first();

        $screenshot = $this->captureService
                        ->height($request->get('height', null))
                        ->width($request->get('width', null))
                        ->delay($request->get('delay', 1))
                        ->url(new Url($request->url))
                        ->capture();

        $apiKey->apiLogs()->create([
            'user_id' => $apiKey->user->id,
            'images' => $screenshot->getPath(),
            'ip_address' => $request->ip(),
        ]);

        return response()->json([
            'data' => [
                'path' => $screenshot->getPublicUrl(),
                'base64' => $screenshot->getBase64(),
            ],
        ]);
    }
}
  • ScreenshotControllerのコンストラクタにてCaptureServiceをDI
  • 前述の通りCaptureServiceはコンストラクタでContract/CanCaptureScreenshotをDIしている
  • デフォルトではChromeBrowserが注入されているので、ここではChromeBrowserのcaputure()が実行される

依存関係
alt

Entity

Url
<?php

namespace Screeenly\Entities;

use Exception;

class Url
{
    /**
     * @var string
     */
    protected $url;

    public function __construct($url)
    {
        $this->url = $url;

        $this->isValid();
    }

    /**
     * Return the sanitized Url.
     * @return string
     */
    public function getUrl()
    {
        return $this->url;
    }

    /**
     * Test if the passed URL has a valid format.
     * @return bool
     */
    protected function isValid()
    {
        if (! filter_var($this->url, FILTER_VALIDATE_URL)) {
            throw new Exception("The URL {$this->url} is invalid.");
        }
    }
}
  • コンストラクタでバリデーションしている
  • Entityとして定義されているがDDDの文脈ではValue Objectの方が正しいのでは?

Screenshot
<?php

namespace Screeenly\Entities;

use Exception;
use Illuminate\Support\Facades\Storage;

class Screenshot
{
    /**
     * @var string
     */
    protected $base64;

    /**
     * @var string
     */
    protected $path;

    /**
     * @var string
     */
    protected $filename;

    /**
     * @var string
     */
    protected $publicUrl;

    /**
     * Screenshot constructor.
     * @param $absolutePath
     * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
     */
    public function __construct($absolutePath)
    {
        $this->doesScreenshotExist($absolutePath);
        $this->path = $absolutePath;
        $this->filename = basename($absolutePath);
        $this->publicUrl = asset(Storage::disk(config('screeenly.filesystem_disk'))->url($this->filename));
        $this->base64 = base64_encode(Storage::disk(config('screeenly.filesystem_disk'))->get($this->filename));
    }

    /**
     * Return base64 representation of the Screenshot.
     * @return string
     */
    public function getBase64()
    {
        return $this->base64;
    }

    /**
     * Return the filename of the Screenshot.
     * @return string
     */
    public function getFilename()
    {
        return $this->filename;
    }

    /**
     * @return string
     */
    public function getPath()
    {
        return $this->path;
    }

    /**
     * Return the public Url to the screenshot image.
     * @return string
     */
    public function getPublicUrl()
    {
        return $this->publicUrl;
    }

    /**
     * Test if a file is available.
     * @param string $absolutePath
     * @return void
     * @throws Exception
     */
    protected function doesScreenshotExist(string $absolutePath)
    {
        if (config('screeenly.filesystem_disk') == 'public') {
            if (file_exists($absolutePath) == false) {
                throw new Exception("Screenshot can't be generated for given URL");
            }
        } else {
            if (Storage::disk(config('screeenly.filesystem_disk'))->exists($absolutePath) == false) {
                throw new Exception("Screenshot can't be generated for given URL");
            }
        }
    }

    /**
     * Delete Screenshot File from Storage.
     * @return bool
     */
    public function delete()
    {
        return Storage::disk(config('screeenly.filesystem_disk'))->delete($this->filename);
    }
}
  • CanCaptureのcaputure()でこのEntityが返却される
  • コンストラクタの引数にスクリーンショットの保存先のstorage pathが渡される
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1