Skip to content

Commit

Permalink
Merge pull request #986 from msmakouz/feature/snapshots-storage
Browse files Browse the repository at this point in the history
[spiral/snapshots] Adding the ability to store snapshots using `Storage` component
  • Loading branch information
butschster committed Sep 29, 2023
2 parents 81d9669 + cb3c5fc commit 4376d23
Show file tree
Hide file tree
Showing 7 changed files with 271 additions and 0 deletions.
47 changes: 47 additions & 0 deletions src/Framework/Bootloader/StorageSnapshotsBootloader.php
@@ -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
@@ -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
@@ -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
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
@@ -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
@@ -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());
}
}
@@ -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);
}
}

0 comments on commit 4376d23

Please sign in to comment.