Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[spiral/snapshots] Adding the ability to store snapshots using Storage component #986

Merged
merged 1 commit into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/Framework/Bootloader/StorageSnapshotsBootloader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Spiral\Bootloader;

use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Boot\EnvironmentInterface;
use Spiral\Core\FactoryInterface;
use Spiral\Snapshots\SnapshotterInterface;
use Spiral\Snapshots\StorageSnapshooter;
use Spiral\Snapshots\StorageSnapshot;
use Spiral\Storage\Bootloader\StorageBootloader;

/**
* Depends on environment variables:
* SNAPSHOTS_BUCKET: bucket name
* SNAPSHOTS_DIRECTORY: where snapshots will be stored in the bucket
* SNAPSHOT_VERBOSITY: defaults to {@see \Spiral\Exceptions\Verbosity::VERBOSE} (1)
*/
final class StorageSnapshotsBootloader extends Bootloader
{
protected const DEPENDENCIES = [
StorageBootloader::class,
];

protected const SINGLETONS = [
StorageSnapshot::class => [self::class, 'storageSnapshot'],
SnapshotterInterface::class => StorageSnapshooter::class,
];

private function storageSnapshot(EnvironmentInterface $env, FactoryInterface $factory): StorageSnapshot
{
$bucket = $env->get('SNAPSHOTS_BUCKET');

if ($bucket === null) {
throw new \RuntimeException(
'Please, configure a bucket for storing snapshots using the environment variable `SNAPSHOTS_BUCKET`.'
);
}

return $factory->make(StorageSnapshot::class, [
'bucket' => $bucket,
'directory' => $env->get('SNAPSHOTS_DIRECTORY', null),
]);
}
}
21 changes: 21 additions & 0 deletions src/Framework/Exceptions/Reporter/StorageReporter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Spiral\Exceptions\Reporter;

use Spiral\Exceptions\ExceptionReporterInterface;
use Spiral\Snapshots\StorageSnapshot;

class StorageReporter implements ExceptionReporterInterface
{
public function __construct(
private StorageSnapshot $storageSnapshot,
) {
}

public function report(\Throwable $exception): void
{
$this->storageSnapshot->create($exception);
}
}
18 changes: 18 additions & 0 deletions src/Framework/Snapshots/StorageSnapshooter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Spiral\Snapshots;

final class StorageSnapshooter implements SnapshotterInterface
{
public function __construct(
private readonly StorageSnapshot $storageSnapshot
) {
}

public function register(\Throwable $e): SnapshotInterface
{
return $this->storageSnapshot->create($e);
}
}
4 changes: 4 additions & 0 deletions src/Snapshots/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"symfony/finder": "^5.3.7|^6.0"
},
"require-dev": {
"spiral/storage": "^3.9",
"phpunit/phpunit": "^10.1",
"vimeo/psalm": "^5.9"
},
Expand All @@ -51,6 +52,9 @@
"dev-master": "3.9.x-dev"
}
},
"suggest": {
"spiral/storage": "For storing snapshots using storage abstraction"
},
"config": {
"sort-packages": true
},
Expand Down
57 changes: 57 additions & 0 deletions src/Snapshots/src/StorageSnapshot.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace Spiral\Snapshots;

use Spiral\Exceptions\ExceptionRendererInterface;
use Spiral\Exceptions\Verbosity;
use Spiral\Storage\StorageInterface;

class StorageSnapshot
{
public function __construct(
protected readonly string $bucket,
protected readonly StorageInterface $storage,
protected readonly Verbosity $verbosity,
protected readonly ExceptionRendererInterface $renderer,
protected readonly ?string $directory = null
) {
}

public function create(\Throwable $e): SnapshotInterface
{
$snapshot = new Snapshot($this->getID($e), $e);

$this->saveSnapshot($snapshot);

return $snapshot;
}

protected function saveSnapshot(SnapshotInterface $snapshot): void
{
$filename = $this->getFilename($snapshot, new \DateTime());

$this->storage
->bucket($this->bucket)
->create($this->directory !== null ? $this->directory . DIRECTORY_SEPARATOR . $filename : $filename)
->write($this->renderer->render($snapshot->getException(), $this->verbosity));
}

/**
* @throws \Exception
*/
protected function getFilename(SnapshotInterface $snapshot, \DateTimeInterface $time): string
{
return \sprintf(
'%s-%s.txt',
$time->format('d.m.Y-Hi.s'),
(new \ReflectionClass($snapshot->getException()))->getShortName()
);
}

protected function getID(\Throwable $e): string
{
return \md5(\implode('|', [$e->getMessage(), $e->getFile(), $e->getLine()]));
}
}
83 changes: 83 additions & 0 deletions src/Snapshots/tests/StorageSnapshotTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

declare(strict_types=1);

namespace Spiral\Tests\Snapshots;

use PHPUnit\Framework\TestCase;
use Spiral\Exceptions\ExceptionRendererInterface;
use Spiral\Exceptions\Verbosity;
use Spiral\Snapshots\StorageSnapshot;
use Spiral\Storage\BucketInterface;
use Spiral\Storage\FileInterface;
use Spiral\Storage\StorageInterface;

final class StorageSnapshotTest extends TestCase
{
private ExceptionRendererInterface $renderer;
private FileInterface $file;
private BucketInterface $bucket;
private StorageInterface $storage;

protected function setUp(): void
{
$this->renderer = $this->createMock(ExceptionRendererInterface::class);
$this->renderer
->expects($this->once())
->method('render')
->willReturn('foo');

$this->file = $this->createMock(FileInterface::class);
$this->file
->expects($this->once())
->method('write')
->with('foo');

$this->bucket = $this->createMock(BucketInterface::class);

$this->storage = $this->createMock(StorageInterface::class);
$this->storage
->expects($this->once())
->method('bucket')
->willReturn($this->bucket);
}

public function testCreate(): void
{
$this->bucket
->expects($this->once())
->method('create')
->with($this->callback(static fn (string $filename) => \str_contains($filename, 'Error.txt')))
->willReturn($this->file);

$e = new \Error('message');
$s = (new StorageSnapshot('foo', $this->storage, Verbosity::VERBOSE, $this->renderer))->create($e);

$this->assertSame($e, $s->getException());

$this->assertStringContainsString('Error', $s->getMessage());
$this->assertStringContainsString('message', $s->getMessage());
$this->assertStringContainsString(__FILE__, $s->getMessage());
$this->assertStringContainsString('53', $s->getMessage());
}

public function testCreateWithDirectory(): void
{
$this->bucket
->expects($this->once())
->method('create')
->with($this->callback(static fn (string $filename) => \str_starts_with($filename, 'foo/bar')))
->willReturn($this->file);

$e = new \Error('message');
$s = (new StorageSnapshot('foo', $this->storage, Verbosity::VERBOSE, $this->renderer, 'foo/bar'))
->create($e);

$this->assertSame($e, $s->getException());

$this->assertStringContainsString('Error', $s->getMessage());
$this->assertStringContainsString('message', $s->getMessage());
$this->assertStringContainsString(__FILE__, $s->getMessage());
$this->assertStringContainsString('72', $s->getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Framework\Bootloader\Exceptions;

use Spiral\Bootloader\StorageSnapshotsBootloader;
use Spiral\Core\Container;
use Spiral\Snapshots\SnapshotterInterface;
use Spiral\Snapshots\StorageSnapshooter;
use Spiral\Snapshots\StorageSnapshot;
use Spiral\Testing\TestApp;
use Spiral\Testing\TestCase;

final class StorageSnapshotsBootloaderTest extends TestCase
{
public const ENV = [
'SNAPSHOTS_BUCKET' => 'foo',
];

public function createAppInstance(Container $container = new Container()): TestApp
{
return TestApp::create(
directories: $this->defineDirectories(
$this->rootDirectory(),
),
handleErrors: false,
container: $container,
)->withBootloaders([StorageSnapshotsBootloader::class]);
}

public function testSnapshotterInterfaceBinding(): void
{
$this->assertContainerBoundAsSingleton(SnapshotterInterface::class, StorageSnapshooter::class);
}

public function testStorageSnapshotBinding(): void
{
$this->assertContainerBoundAsSingleton(StorageSnapshot::class, StorageSnapshot::class);
}
}