Dev BMW

SignatureCheckListener With Symfony’s AsEventListener Attribute


# .env file
APP_CHECK_SIGNATURE=true

<?php

namespace App\Listener;

use App\Enum\Headers;
use App\Exception\UnmatchedSignatureException;
use App\Service\SignatureService;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

#[AsEventListener]
class SignatureCheckListener
{
    public function __construct(
        #[Autowire(env: 'bool:APP_CHECK_SIGNATURE')]
        private readonly bool                  $checkSignature,
        private readonly TokenStorageInterface $tokenStorage,
        private readonly SignatureService      $signatureService,
        private readonly LoggerInterface       $logger,
    )
    {

    }

    /**
     * @param RequestEvent $event
     * @return void
     * @throws UnmatchedSignatureException
     */
    public function __invoke(RequestEvent $event): void
    {
        if (!$this->checkSignature) {
            return;
        }
        $request = $event->getRequest();

        $signature = $request->headers->get(Headers::SIGNATURE->value) ?? '';
        $user = $this->tokenStorage->getToken()?->getUser();

        if (!$this->signatureService->check($user, $request, $signature)) {
            $this->logger->warning(sprintf('Unmatched Signature: %s', $signature));
            throw new UnmatchedSignatureException();
        }
    }
}

The AsEventListener attribute accepts the following parameters.
/symfony/event-dispatcher/Attribute/AsEventListener.php
/**
     * @param string|null $event      The event name to listen to
     * @param string|null $method     The method to run when the listened event is triggered
     * @param int         $priority   The priority of this listener if several are declared for the same event
     * @param string|null $dispatcher The service id of the event dispatcher to listen to
     */

Storing the header parameters in a PHP Enum can be very useful for easier management.
<?php

namespace App\Enum;

enum Headers: string
{
    case SIGNATURE = 'X-Signature';
}

We’re creating a service to calculate the signature.
<?php

namespace App\Service;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\User\UserInterface;

class SignatureService
{
    public function __construct()
    {

    }

    public function check(?UserInterface $user, Request $request, string $signature): bool
    {
        if (hash_equals($signature, $this->calculateSignature($user, $request))) {
            return true;
        }
        return false;
    }

    private function calculateSignature(?UserInterface $user, Request $request): string
    {
        //your algorithm, this is example
        return 'your_calculated_signature';
    }
}

Essentially, we aim to re-authenticate a request encrypted with a flag known to both the user and the server, thereby protecting the request from MITM (Man-in-the-middle) attacks. You can explore the Diffie-Hellman algorithm to securely store a private key on the user side without transmitting it over the network.
1 месяц назад

Symfony Scoped HttpClient


# .env file
SLACK_WEBHOOK_BASE_URL='https://hooks.slack.com'
APP_SLACK_WEBHOOK_YOUR_CHANNEL_ADDRESS='your channel address'

We define the required settings in the env file.
// config/packages/framework.yaml
framework:
    ...
    http_client:
        scoped_clients:
            slack_webhook_client:
                base_uri: '%env(resolve:SLACK_WEBHOOK_BASE_URL)%'
                timeout: 2.0*/

We define scoped HTTP clients in our framework configuration file. I couldn’t find any Attribute usage for this in the documentation.
<?php
namespace App\Service;

use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Throwable;

readonly class SlackService
{
    public function __construct(
        #[Autowire(service: 'slack_webhook_client')]
        private HttpClientInterface $slackClient,
        #[Autowire(env: 'APP_SLACK_WEBHOOK_YOUR_CHANNEL_ADDRESS')]
        private string              $yourChannelAddress,
        private LoggerInterface     $logger
    )
    {
    }

    public function sendText(string $text): void
    {
        try {
            $this->slackClient->request(
                Request::METHOD_POST,
                $this->yourChannelAddress,
                [
                    'json' => ['text' => $text],
                ]
            );
        } catch (Throwable $exception) {
            $this->logger->warning($exception->getMessage());
        }
    }
}
1 месяц назад

How to work with archives in PHP


Archive opening example:
<?php
...

$zip = new ZipArchive();

$archivePath = $archive->getPathname();
if (true !== $zip->open($archivePath)) {
    throw new \RuntimeException(sprintf('Cannot open archive "%s".', $archivePath));
}

...

Archive files iterating example (without extracting):
<?php
...

/** @var ZipArchive $zip */
for ($i = 0; $i < $zip->numFiles; ++$i) {
    $fileName = $zip->getNameIndex($i);

    $stream = $zip->getStream($fileName);
    if ($stream) {
        $content = stream_get_contents($stream);
        // do something with content
        // if content is text - you can easily work with them!
        fclose($stream);
    }
}
...

Archive extracting & recursive files iterating (via RecursiveIteratorIterator and RecursiveDirectoryIterator) example:
<?php
...

// extraction
$tempDir = sys_get_temp_dir() . sprintf('/%s', 'archive_') . uniqid('', true);
if (false === @mkdir($tempDir, 0777, true) && !is_dir($tempDir)) {
    throw new RuntimeException(sprintf('Unable to create the "%s" directory.', $tempDir));
}

$zip->extractTo($tempDir);
$zip->close();

// iteration
/** @var \SplFileInfo $file */
foreach ($filesFirstIterator as $file) {
    $pathName = $file->getPathname();

    if ($file->isDir()) {
        // do something
    }

    if($file->isFile()) {
        // do domething
    }
}

And make sure that after any actions with extracted files you will delete temporary directory with the extracted archive. I suggest doing this in finally construction in case there is any error during your code work:
<?php
    ...
    public function uploadArchiveAction(Request $request): Response
    {
        try {
            // your logic
        } catch (\Throwable $exception) {
            // do any actions
        } finally {
            $this->deleteDirectory($tempDir);
        }
    }


    private function deleteDirectory(string $dir): void
    {
        if (!is_dir($dir)) {
            return;
        }

        $filesFirst = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
            RecursiveIteratorIterator::CHILD_FIRST
        );

        foreach ($filesFirst as $file) {
            $pathName = $file->getPathname();

            if ($file->isDir()) {
                rmdir($pathName);
            } else {
                unlink($pathName);
            }
        }

        rmdir($dir);
    }
1 месяц назад

Symfony S3 Service Encapsulation


composer require aws/aws-sdk-php
# .env file
S3_CREDENTIALS_KEY='your key'
S3_CREDENTIALS_SECRET='your secret'
S3_REGION='your region'
S3_VERSION='your version'
S3_ENDPOINT=''
S3_USE_PATH_STYLE_ENDPOINT=''

S3Config.php
<?php

namespace App\Service\S3;

use Symfony\Component\DependencyInjection\Attribute\Autowire;

readonly class S3Config
{
    public function __construct(
        #[Autowire(env: 'S3_CREDENTIALS_KEY')]
        public string $credentialKey,
        #[Autowire(env: 'S3_CREDENTIALS_SECRET')]
        public string $credentialSecret,
        #[Autowire(env: 'S3_REGION')]
        public string $region,
        #[Autowire(env: 'S3_VERSION')]
        public string $version,
        #[Autowire(env: 'S3_ENDPOINT')]
        public string $endpoint,
        #[Autowire(env: 'bool:S3_USE_PATH_STYLE_ENDPOINT')]
        public bool $usePathStyleEndpoint,
    )
    {
    }
}

S3Service.php
<?php

namespace App\Service\S3;

use Aws\Credentials\Credentials;
use Aws\S3\S3Client;

readonly class S3Service
{
    private S3Client $s3Client;

    public function __construct(
        private S3Config $config
    )
    {
        $parameters = [
            'credentials' => new Credentials($this->config->credentialKey, $this->config->credentialSecret),
            'region' => $this->config->region,
            'version' => $this->config->version,
            'use_path_style_endpoint' => $this->config->usePathStyleEndpoint,
        ];
        if (!empty($this->config->endpoint)) {
            $parameters['endpoint'] = $this->config->endpoint;
        }

        $this->s3Client = new S3Client($parameters);
    }

    public function listBuckets(): array
    {
        $buckets = $this->s3Client->listBuckets();
        return $buckets['Buckets'];
    }

    public function putObjectByBody(string $bucket, string $key, string $body): void
    {
        $this->s3Client->putObject([
            'Bucket' => $bucket,
            'Key' => $key,
            'Body' => $body
        ]);
    }

    public function getObject(string $bucket, string $key): ?string
    {
        $result = $this->s3Client->getObject([
            'Bucket' => $bucket,
            'Key' => $key
        ]);

        return $result['Body'] ?? null;
    }
}
1 месяц назад

Lazy tagged iterator in Symfony


One interface for services
Let’s start from an interface for our lazy services:
<?php

declare(strict_types=1);

namespace App\Model\Car;

use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

#[AutoconfigureTag]
interface CarInterface
{
    public function drive(): void;
}
Pay attention at AutoconfigureTag. This attribute helps symfony autowire all services inside container.

Next, we will create a few implementations for our interface:
<?php

declare(strict_types=1);

namespace App\Model\Car;

use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;

#[AsTaggedItem('bmw')]
class BmwCar implements CarInterface
{
    public function drive(): void
    {
        dump('Drive. Brrrrrr');
    }
}
<?php

declare(strict_types=1);

namespace App\Model\Car;

use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;

#[AsTaggedItem('audi')]
class AudiCar implements CarInterface
{
    public function drive(): void
    {
        dump('crash engine or etc.');
    }
}
<?php

declare(strict_types=1);

namespace App\Model\Car;

use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;

#[AsTaggedItem('mazda')]
class MazdaCar implements CarInterface
{
    public function drive(): void
    {
        dump('Drive and drive.');
    }
}
As you can see in each implementation we have AsTaggedItem attribute. Services can be injected inside container without this attribute, but naming will be like:
array:3 [▼
  "App\Model\Car\AudiCar" => "App\Model\Car\AudiCar"
  "App\Model\Car\BmwCar" => "App\Model\Car\BmwCar"
  "App\Model\Car\MazdaCar" => "App\Model\Car\MazdaCar"
]
With AsTaggedItem attribute naming will be:
array:3 [▼
  "audi" => "App\Model\Car\AudiCar"
  "bmw" => "App\Model\Car\BmwCar"
  "mazda" => "App\Model\Car\MazdaCar"
]
Service locator for our interface implementations
So, it is time to use our lazy service locator! Create class, that requires service locator:
<?php

declare(strict_types=1);

namespace App\Services\Provider;

use App\Model\Car\CarInterface; 
use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;
use Symfony\Contracts\Service\ServiceCollectionInterface;

class CarProvider
{
    public function __construct(
        #[AutowireLocator(
            services: CarInterface::class,
        )]
        private ServiceCollectionInterface $carServiceLocator,
    ) {
    }

    public function driveCar(string $carType): void
    {
        if (!$this->carServiceLocator->has($carType)) {
            throw new \InvalidArgumentException(sprintf(
                'Car for type "%s" not found!',
                $carType,
            ));
        }

        /** @var CarInterface $car */
        $car = $this->carServiceLocator->get($carType);

        $car->drive();
    }

    /**
     * @return array<string, string>
     */
    public function getAvailableCars(): array
    {
        return $this->carServiceLocator->getProvidedServices();
    }
}
where AutowireLocator is a convenient attribute which helps us show symfony what services we need. The first argument is name our CarInterface (it can also be a simple array of services). With AutowireLocator attribute, you can configure key and priority of your services. It also supports configuring via static methods.

Method below returns a list of key => service name for our locator and prevents initializing all services in locator.
/**
     * @return array<string, string>
     */
    public function getAvailableCars(): array
    {
        return $this->carServiceLocator->getProvidedServices();
    }
You can use array keys for getting any service from locator.

Method driveCar shows an example of using locator in your application:
public function driveCar(string $carType): void
    {
        if (!$this->carServiceLocator->has($carType)) {
            throw new \InvalidArgumentException(sprintf(
                'Car for type "%s" not found!',
                $carType,
            ));
        }

        /** @var CarInterface $car */
        $car = $this->carServiceLocator->get($carType);

        $car->drive();
    }
You can check and get service by service key with service locator.

Using service locator in a real example
Below, you can see an example of using service locator in your app:
<?php

declare(strict_types=1);

namespace App\Controller;

use App\Services\Provider\CarProvider;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;

class ExampleController extends AbstractController
{
    #[Route(
        path: '/{carType?}',
        defaults: [
            'carType' => 'bmw',
        ]
    )]
    public function mainAction(
        CarProvider $carProvider,
        string $carType,
    ): JsonResponse {
        $carProvider->driveCar($carType);

        return new JsonResponse($carProvider->getAvailableCars());
    }
}

Improvements
You can store service keys in static methods or in php enum and use them for fetching services from container and avoiding code duplication, but it is not the theme of this article.

See full example on https://github.com/no4ch/lazy-tagged-iterator-example

In the end, you can find more information about this feature in official documentation https://symfony.com/doc/7.1/service_container/service_subscribers_locators.html

Thanks for the reading!
2 месяца назад

Найти файлы по ключу и сразу запаковать их в архив


grep -rlZ 'text to search' . | tar --null -czvf archive.tar.gz --files-from=-
7 месяцев назад

Symfony slug в URL


Есть 2 варианта:
Первый через MapEntity:
#[Route('/article/{slug}', name: 'blog_article', methods: ['GET'])]
public function article(
    #[MapEntity(mapping: ['slug' => 'slug'])] Article $article
): Response {
    return $this->render('blog/article/show.html.twig', [
        'article' => $article,
    ]);
}

Второй вариант через doctrine.yaml:
doctrine:
    orm:
        controller_resolver:
            auto_mapping: true
тогда:
#[Route('/article/{slug}', name: 'blog_article_show', methods: ['GET'])]
public function article(
    Article $article
): Response {
    return $this->render('blog/article/show.html.twig', [
        'article' => $article,
    ]);
}
8 месяцев назад

Надежный ssh ключ

ssh

ssh-keygen -o -a 100 -t ed25519 -C "byvlad@me.com"
6 лет назад

Работа с Tar


# создать .tar
tar -cvf file.tar /full/path
# создать .tar.gz (архив)
tar -czvf file.tar.gz /full/path
# создать .tar.bz2 (архив)
tar -cjvf file.tar.bz2 /full/path

# распаковка
tar -xvf file.tar.gz
7 лет назад