Symfony - Best practice to reset the database - php

I'm working on a Symfony 4.2 project and I'm looking for the best practice to achieve a reset of the database when the admin needs to do it via a button in backoffice.
Explanation :
The project is a temporary event website.
This means that, people will visit the website only for a day / a week and then the website is off. Per example, a website for spectators into a stadium during a basketball tournament.
When the tournament is over, the administrator would like to reset all datas sent during it via a button.
Right now I did it like this but I don't know if it's the better way in production environment.
I created a service which get the KernelInterface in constructor :
public function resetDB() {
$application = new Application($this->kernel);
$application->setAutoExit(false);
$input = new ArrayInput([
'command' => 'doctrine:schema:drop',
'--force' => true
]);
$output = new BufferedOutput();
$application->run($input, $output);
$responseDrop = $output->fetch();
if (strpos($responseDrop, 'successfully') !== false) {
$input = new ArrayInput([
'command' => 'doctrine:schema:create',
]);
$application->run($input, $output);
$responseCreate = $output->fetch();
if (strpos($responseCreate, 'successfully') !== false)
return new Response();
}
return new \ErrorException();
}
Firstly, is it good to do it like this in a production environment ? (Nobody else the administrator will use the website when doing this operation)
Secondly, I'm not really satisfied with the method I used to check if the operation has been successfully done (strpos($responseCreate, 'successfully') !== false). Does someone know a better way ?
Thanks a lot for your help

If it works for you, its ok. About the "successful" check part. Simply surround your call in a try-catch block and check for exceptions. If no exception was thrown, assume it did execute successfully.
$application = new Application($this->kernel);
$application->setAutoExit(false);
try {
$application->run(
new StringInput('doctrine:schema:drop --force'),
new DummyOutput()
);
$application->run(
new StringInput('doctrine:schema:create'),
new DummyOutput()
);
return new Response();
} catch (\Exception $exception) {
// don't throw exceptions, use proper responses
// or do whatever you want
return new Response('', Response::HTTP_INTERNAL_SERVER_ERROR);
}
Is PostgreSQL good enough at DDL transactions? Force a transaction then:
$application = new Application($this->kernel);
$application->setAutoExit(false);
// in case of any SQL error
// an exception will be thrown
$this->entityManager->transactional(function () use ($application) {
$application->run(
new StringInput('doctrine:schema:drop --force'),
new DummyOutput()
);
$application->run(
new StringInput('doctrine:schema:create'),
new DummyOutput()
);
});
return new Response();

I'm not sure about the way you executing the commands, but there is a single command alternative to consider, using DoctrineFixturesBundle. You need to install it for use in the production environment (technically not recommended, I think because of risk of deleting prod data, but that's what you want to do).
Install:
$ composer require doctrine/doctrine-fixtures-bundle
Config:
// config/bundles.php
return [
...
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['all' => true],
...
];
You do need to create a fixture and it must have a load() method that is compatible with Doctrine\Common\DataFixtures\FixtureInterface::load(Doctrine\Common\Persistence\ObjectManager $manager), but it can be empty exactly like below:
<?php // src/DataFixtures/AppFixtures.php
namespace App\DataFixtures;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager){}
}
The command:
$ php bin/console doctrine:fixtures:load -n --purge-with-truncate --env=prod
Help:
$ php bin/console doctrine:fixtures:load --help

Firstly, is it good to do it like this in a production environment ?
I don't think so! For example the commands below would warn you with: [CAUTION] This operation should not be executed in a production environment!. However, everything is possible in wild programming world as shown below.
Try Symfony's The Process Component.
This is the basic example so it is up to you make it cleaner and duplication free. I tested and it works. You can stream the output as well.
# DROP IT
$process = new Process(
['/absolute/path/to/project/bin/console', 'doctrine:schema:drop', '--force', '--no-interaction']
);
$process->run();
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}
# RECREATE IT
$process = new Process(
['/absolute/path/to/project/bin/console', 'doctrine:schema:update', '--force', '--no-interaction']
);
$process->run();
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}

Related

PHP SDK not sending errors to Sentry when invoked from IBM Cloud Functions

I am using Serverless framework to deploy my PHP code as IBM Cloud Function.
Here is the code from the action PHP file:
function main($args): array {
Sentry\init(['dsn' => 'SENTRY_DSN' ]);
try {
throw new \Exception('Some error')
} catch (\Throwable $exception) {
Sentry\captureException($exception);
}
}
And this is the serverless.yml file:
service: cloudfunc
provider:
name: openwhisk
runtime: php
package:
individually: true
exclude:
- "**"
include:
- "vendor/**"
functions:
test-sentry:
handler: actions/test-sentry.main
annotations:
raw-http: true
events:
- http:
path: /test-sentry
method: post
resp: http
package:
include:
- actions/test-sentry.php
plugins:
- serverless-openwhisk
When I test the action handler from my local environment(NGINX/PHP Docker containers) the errors are being sent to Sentry.
But when I try to invoke the action from IBM Cloud nothing appears in the Sentry console.
Edit:
After some time trying to investigate the source of the problem I saw that its related with the async nature of sending the http request to Sentry(I have other libraries that make HTTP/TCP connections to Loggly, RabbitMQ, MySQL and they all work as expected):
vendor/sentry/sentry/src/Transport/HttpTransport.php
in the send method where the actual http request is being sent:
public function send(Event $event): ?string
{
$request = $this->requestFactory->createRequest(
'POST',
sprintf('/api/%d/store/', $this->config->getProjectId()),
['Content-Type' => 'application/json'],
JSON::encode($event)
);
$promise = $this->httpClient->sendAsyncRequest($request);
//The promise state here is "pending"
//This line here is being logged in the stdout of the invoked action
var_dump($promise->getState());
// This function is defined in-line so it doesn't show up for type-hinting
$cleanupPromiseCallback = function ($responseOrException) use ($promise) {
//The promise state here is "fulfilled"
//This line here is never logged in the stdout of the invoked action
//Like the execution never happens here
var_dump($promise->getState());
$index = array_search($promise, $this->pendingRequests, true);
if (false !== $index) {
unset($this->pendingRequests[$index]);
}
return $responseOrException;
};
$promise->then($cleanupPromiseCallback, $cleanupPromiseCallback);
$this->pendingRequests[] = $promise;
return $event->getId();
}
The requests that are registered asynchronously are sent in the destructor of the HttpTransport instance or when PHP shuts down as a shutdown function is registered. In OpenWhisk we never shut down as we run in a never-ending loop until the Docker container is killed.
Update: You can now call $client-flush() and don't need to worry about reflection.
main() now looks like this:
function main($args): array {
Sentry\init(['dsn' => 'SENTRY_DSN' ]);
try {
throw new \Exception('Some error')
} catch (\Throwable $exception) {
Sentry\captureException($exception);
}
$client = Sentry\State\Hub::getCurrent()->getClient();
$client->flush();
return [
'body' => ['result' => 'ok']
];
}
Original explanation:
As a result, to make this work, we need to call the destructor of the $transport property of the Hub's $client. Unfortunately, this private, so the easiest way to do this is to use reflection to make it visible and then call it:
$client = Sentry\State\Hub::getCurrent()->getClient();
$property = (new ReflectionObject($client))->getProperty('transport');
$property->setAccessible(true);
$transport = $property->getValue($client);
$transport->__destruct();
This will make the $transport property visible so that we can retrieve it and call its destructor which will in turn call cleanupPendingRequests() that will then send the requests to sentry.io.
The main() therefore looks like this:
function main($args): array {
Sentry\init(['dsn' => 'SENTRY_DSN' ]);
try {
throw new \Exception('Some error')
} catch (\Throwable $exception) {
Sentry\captureException($exception);
}
$client = Sentry\State\Hub::getCurrent()->getClient();
$property = (new ReflectionObject($client))->getProperty('transport');
$property->setAccessible(true);
$transport = $property->getValue($client);
$transport->__destruct();
return [
'body' => ['result' => 'ok']
];
}
Incidentally, I wonder if this Sentry SDK works with Swoole?
Function runtimes are "paused" between requests by the platform. This means any background processes will be blocked if they aren't finished when the function returns.
It looks like the asynchronous HTTP request doesn't get a chance to complete before the runtime pauses.
You will need to find some way to block returning from the function until that request is completed. If the Sentry SDK has some callback handler or other mechanism to be notified when messages have been sent, you could use that?

I can't add sentry to php slim

I need to add php sentry error handler to my slim 3 project.
how can I do so ?
where should put sentry integration code?
what I'm doing now is :
// monolog
$container['logger'] = function ($c) {
$settings = $c->get('settings')['logger'];
$logger = new Monolog\Logger($settings['name']);
$logger->pushProcessor(new Monolog\Processor\UidProcessor());
$logger->pushHandler(new Monolog\Handler\StreamHandler($settings['path'], $settings['level']));
$client = new Raven_Client(
'http://key#ip:9000/2'
);
$handler = new Monolog\Handler\RavenHandler($client);
$handler->setFormatter(new Monolog\Formatter\LineFormatter("%message% %context% %extra%\n"));
$logger->pushHandler($handler);
return $logger;
};
but I'm not getting all errors in my sentry dashboard.
for example accessing undefined array indexes.
thanks.
I think the best way is to just do the following (I did not test this or have ever used Slim but looking at the Slim docs this is a way to do it):
In your index.php (which should be the app entrypoint) just after require '../../vendor/autoload.php'; (the composer autoload).
Add the Raven initialization code:
$sentry = new Raven_Client('http://key#ip:9000/2');
$sentry->install();
This will configure the SDK to handle (and send) all errors, no need for the Monolog handler anymore.
If you want to integration it in a ErrorHandler class you created looking at this skeleton project might give you some ideas.
I am using a custom error handler to catch exceptions. This way i can use the default slim error handler and Sentry error reporting at the same time.
This is my code:
// initalize sentry
Sentry\init(['dsn' => 'your_dsn' ]);
// Run app
$app = (new App())->get();
// register custom error handler
$c = $app->getContainer();
$c['errorHandler'] = function ($c) {
return function ($request, $response, $exception) use ($c) {
// send error to sentry
Sentry\captureException($exception);
// invoke default error handler
$handler = new Slim\Handlers\Error();
return $handler->__invoke($request, $response, $exception);
};
};
$app->run();
Not sure if this is the "recommended" way, but it works.

How to use a caching system (memcached, redis or any other) with slim 3

I browsed the internet and didn't find much information on how to use any caching library with Slim framework 3.
Can anyone help me with this issue?
I use symfony/cache with Slim 3. You can use any other cache library, but I give an example setup for this specific library. And I should mention, this is actually independent of Slim or any other framework.
First you need to include this library in your project, I recommend using composer. I also will iinclude predis/predis to be able to use Redis adapter:
composer require symfony/cache predis/predis
Then I'll use Dependency Injection Container to setup cache pool to make it available to other objects which need to use caching features:
// If you created your project using slim skeleton app
// this should probably be placed in depndencies.php
$container['cache'] = function ($c) {
$config = [
'schema' => 'tcp',
'host' => 'localhost',
'port' => 6379,
// other options
];
$connection = new Predis\Client($config);
return new Symfony\Component\Cache\Adapter\RedisAdapter($connection);
}
Now you have a cache item pool in $container['cache'] which has methods defined in PSR-6.
Here is a sample code using it:
class SampleClass {
protected $cache;
public function __construct($cache) {
$this->cache = $cache;
}
public function doSomething() {
$item = $this->cache->getItem('unique-cache-key');
if ($item->isHit()) {
return 'I was previously called at ' . $item->get();
}
else {
$item->set(time());
$item->expiresAfter(3600);
$this->cache->save($item);
return 'I am being called for the first time, I will return results from cache for the next 3600 seconds.';
}
}
}
Now when you want to create new instance of SampleClass you should pass this cache item pool from the DIC, for example in a route callback:
$app->get('/foo', function (){
$bar = new SampleClass($this->get('cache'));
return $bar->doSomething();
});
$memcached = new \Memcached();
$memcached->addServer($cachedHost, $cachedPort);
$metadataCache = new \Doctrine\Common\Cache\MemcachedCache();
$metadataCache->setMemcached($memcached);
$queryCache = new \Doctrine\Common\Cache\MemcachedCache();
$queryCache->setMemcached($memcached);

Is this data being overwritten by another component?

I'm doing some programming in Silex with the symfony components and I think I have found a bug with the symfony/serializer and the symfony/validator components.
First let me explain what I'm traing to achieve, then let's go to the code.
My objective is to annotate a class with information like serialization directives as well as validation directives. As the reading of these annotations can cost a litle cpu, I like to cache them in memory. For this purpose, I'm using memcache wrapper in the Doctrine/Common/Cache package.
The problem I face is that both the symfony/serializer and the symfony/validator write Metadata to the cache using the class name as key. When they try to retrieve the metadata later, they throw an exception, because the cache has invalid metadata, either an instance of Symfony\Component\Validator\Mapping\ClassMetadata or Symfony\Component\Serializer\Mapping\ClassMetadataInterface.
Following is a reproductible example (sorry if its big, I tried to make as small as possible):
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
class Foo
{
/**
* #var int
* #Assert\NotBlank(message="This field cannot be empty")
*/
private $someProperty;
/**
* #return int
* #Groups({"some_group"})
*/
public function getSomeProperty() {
return $this->someProperty;
}
}
use Doctrine\Common\Annotations\AnnotationReader;
use \Memcache as MemcachePHP;
use Doctrine\Common\Cache\MemcacheCache as MemcacheWrapper;
$loader = require_once __DIR__ . '/../vendor/autoload.php';
\Doctrine\Common\Annotations\AnnotationRegistry::registerLoader([$loader, 'loadClass']);
$memcache = new MemcachePHP();
if (! $memcache->connect('localhost', '11211')) {
throw new \Exception('Unable to connect to memcache server');
}
$cacheDriver = new MemcacheWrapper();
$cacheDriver->setMemcache($memcache);
$app = new \Silex\Application();
$app->register(new Silex\Provider\SerializerServiceProvider());
$app['serializer.normalizers'] = function () use ($app, $cacheDriver) {
$classMetadataFactory = new Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory(
new Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader(new AnnotationReader()), $cacheDriver);
return [new Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer($classMetadataFactory) ];
};
$app->register(new Silex\Provider\ValidatorServiceProvider(), [
'validator.mapping.class_metadata_factory' =>
new \Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory(
new \Symfony\Component\Validator\Mapping\Loader\AnnotationLoader(new AnnotationReader()),
new \Symfony\Component\Validator\Mapping\Cache\DoctrineCache($cacheDriver)
)
]);
$app->get('/', function(\Silex\Application $app) {
$foo = new Foo();
$app['validator']->validate($foo);
$json = $app['serializer']->serialize($foo, 'json');
return new \Symfony\Component\HttpFoundation\JsonResponse($json, \Symfony\Component\HttpFoundation\Response::HTTP_OK, [], true);
});
$app->error(function (\Exception $e, \Symfony\Component\HttpFoundation\Request $request, $code) {
return new \Symfony\Component\HttpFoundation\Response('We are sorry, but something went terribly wrong.' . $e->getMessage());
});
$app->run();
After running this example you get fatal errors.
Can anyone confirm that I'm not making a hard mistake here?
Currently my workaround for this is rewrite the DoctrineCache class making use of a namespace for the cache keys. Its working, but I think its ugly.
I think what you need to do is two separate CacheDrivers. See https://github.com/doctrine/cache/blob/master/lib/Doctrine/Common/Cache/CacheProvider.php for how namespaces are used there.
You could:
$validatorCacheDriver = new MemcacheWrapper();
$validatorCacheDriver->setMemcache($memcache);
$validatorCacheDriver->setNamespace('symfony_validator');
$serializerCacheDriver = new MemcacheWrapper();
$serializerCacheDriver->setMemcache($memcache);
$serializerCacheDriver->setNamespace('symfony_serializer');
// note that the two drivers are using the same memcache instance,
// so only one connection will be used.
$app['serializer.normalizers'] = function () use ($app, $serializerCacheDriver) {
$classMetadataFactory = new Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory(
new Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader(new AnnotationReader()), $serializerCacheDriver);
return [new Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer($classMetadataFactory) ];
};
$app->register(new Silex\Provider\ValidatorServiceProvider(), [
'validator.mapping.class_metadata_factory' =>
new \Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory(
new \Symfony\Component\Validator\Mapping\Loader\AnnotationLoader(new AnnotationReader()),
new \Symfony\Component\Validator\Mapping\Cache\DoctrineCache($validatorCacheDriver)
)
]);
I've trimmed the code to only show the parts that play some part in the solution. I hope this helps!

Laravel 5 console (artisan) command unit tests

I am migrating my Laravel 4.2 app to 5.1 (starting with 5.0) and am a lot of trouble with my console command unit tests. I have artisan commands for which I need to test the produced console output, proper question/response handling and interactions with other services (using mocks). For all its merits, the Laravel doc is unfortunately silent with regards to testing console commands.
I finally found a way to create those tests, but it feels like a hack with those setLaravel and setApplication calls.
Is there a better way to do this? I wish I could add my mock instances to the Laravel IoC container and let it create the commands to test with everything properly set. I'm afraid my unit tests will break easily with newer Laravel versions.
Here's my unit test:
Use statements:
use Mockery as m;
use App\Console\Commands\AddClientCommand;
use Symfony\Component\Console\Tester\CommandTester;
Setup
public function setUp() {
parent::setUp();
$this->store = m::mock('App\Services\Store');
$this->command = new AddClientCommand($this->store);
// Taken from laravel/framework artisan command unit tests
// (e.g. tests/Database/DatabaseMigrationRollbackCommandTest.php)
$this->command->setLaravel($this->app->make('Illuminate\Contracts\Foundation\Application'));
// Required to provide input to command questions (provides command->getHelper())
// Taken from ??? when I first built my command tests in Laravel 4.2
$this->command->setApplication($this->app->make('Symfony\Component\Console\Application'));
}
Input provided as command arguments. Checks console output
public function testReadCommandOutput() {
$commandTester = new CommandTester($this->command);
$result = $commandTester->execute([
'--client-name' => 'New Client',
]);
$this->assertSame(0, $result);
$templatePath = $this->testTemplate;
// Check console output
$this->assertEquals(1, preg_match('/^Client \'New Client\' was added./m', $commandTester->getDisplay()));
}
Input provided by simulated keyboard keys
public function testAnswerQuestions() {
$commandTester = new CommandTester($this->command);
// Simulate keyboard input in console for new client
$inputs = $this->command->getHelper('question');
$inputs->setInputStream($this->getInputStream("New Client\n"));
$result = $commandTester->execute([]);
$this->assertSame(0, $result);
$templatePath = $this->testTemplate;
// Check console output
$this->assertEquals(1, preg_match('/^Client \'New Client\' was added./m', $commandTester->getDisplay()));
}
protected function getInputStream($input) {
$stream = fopen('php://memory', 'r+', false);
fputs($stream, $input);
rewind($stream);
return $stream;
}
updates
This doesn't work in Laravel 5.1 #11946
I have done this before as follows - my console command returns a json response:
public function getConsoleResponse()
{
$kernel = $this->app->make(Illuminate\Contracts\Console\Kernel::class);
$status = $kernel->handle(
$input = new Symfony\Component\Console\Input\ArrayInput([
'command' => 'test:command', // put your command name here
]),
$output = new Symfony\Component\Console\Output\BufferedOutput
);
return json_decode($output->fetch(), true);
}
So if you want to put this in it's own command tester class, or as a function within TestCase etc... up to you.
use Illuminate\Support\Facades\Artisan;
use Symfony\Component\Console\Output\BufferedOutput;
$output = new BufferedOutput();
Artisan::call('passport:client', [
'--password' => true,
'--name' => 'Temp Client',
'--no-interaction' => true,
], $output);
$stringOutput = $output->fetch();

Categories