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'));
}
}
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()が実行される
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が渡される