In case if you need to test PHP application error handlers, you have to "mock" or just disable sending errors on remote servers in the Sentry client. What is the right way to do this?
This is the example for Laravel, but this approach should work for any framework.
use Sentry\Client;
use Sentry\EventFactory;
use Sentry\Options;
use Sentry\Serializer\RepresentationSerializer;
use Sentry\Serializer\Serializer;
use Sentry\Transport\NullTransport;
private function mockSentry(): void
{
/** #var \Sentry\State\Hub $sentry */
$sentry = $this->app['sentry']; // Get sentry object from Laravel's container
$client = new Client(
new Options(),
new NullTransport(),
new EventFactory(
new Serializer(new Options()),
new RepresentationSerializer(new Options()),
new Options(),
'1',
'1',
),
);
$sentry->bindClient($client);
}
Related
I am working on Laravel 9 where I install Laravel WebSocket, Laravel Echo, and Pusher PHP server.
By the way, I didn't use the official Pusher application, just using the package as per Laravel-WebSocket package documentation suggested.
User case - I want to update the site model value and send a notification (broadcast and mail) to the end user as soon as the user deletes a site.
Everything is installed and working fine but I found some glitches in the Laravel-WebSocket, Pusher package.
I have created the following event which will broadcast to the end user.
SiteDelete.php
<?php
namespace App\Events;
use App\Models\Site;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class SiteDeleted implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* The site instance.
*
* #var \App\Models\Site
*/
public $site;
/**
* The name of the queue connection to use when broadcasting the event.
*
* #var string
*/
public $connection = 'database';
/**
* The name of the queue on which to place the broadcasting job.
*
* #var string
*/
public $queue = 'default';
/**
* Create a new event instance.
*
* #return void
*/
public function __construct(Site $site)
{
$this->site = $site;
}
/**
* Get the channels the event should broadcast on.
*
* #return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
// return new PrivateChannel('site.delete.'.$this->site->id); // this is not working.
// return [new PrivateChannel('site.delete.'.$this->site->id)]; // this is not working.
return [new PrivateChannel('site.delete.'.$this->site->id), new Channel('mock')]; // This will call but I need to pass two channels intentionally.
}
/**
* Get the data to broadcast.
*
* #return array
*/
public function broadcastWith()
{
return ['id' => $this->site->id];
}
}
app.js
Echo.private("site.delete.1")
.listen('SiteDeleted', (e) => {
console.log("SiteDeleted");
console.log(JSON.stringify(e));
})
Echo.private('App.Models.User.7')
.notification((notification) => {
console.log("App.Models.User");
console.log(notification);
});
Problem
As you can see comments in my event class's broadcastOn method where I need to pass two channels. One is real and the second one is fake. So ultimately you need to pass at least two channels so the pusher request will have a channels parameter [which will work] but the channel parameter never works[i.e when you pass a single channel].
I can able to send custom events from the WebSocket GUI. i.e from http://localhost:8000/laravel-websockets URL. but those events are never caught by the front end unless I do it the dirty way.
The notifications are never caught by the front end due to this channel and channels parameter issue.
Dirty Way[Yes I know we should never touch the vendor folder but just curious to know why the things are not working]
I checked the vendor folder very deeply and I come to know, in the vendor/pusher/pusher-php-server/src/Pusher.php under the make_event function if I update the following line then it starts working without passing two channels.
vendor/pusher/pusher-php-server/src/Pusher.php
private function make_event(array $channels, string $event, $data, array $params = [], ?string $info = null, bool $already_encoded = false): array
{
// if (count($channel_values) == 1) {
// $post_params['channel'] = $channel_values[0];
// } else {
// $post_params['channels'] = $channel_values;
// }
$post_params['channels'] = $channel_values;
}
My Research
As the WebSocket package suggests installing pusher-php-server version 3.0 but I install the latest one i.e 7. Version 3.0 is incompatible with Laravel 9. But I can't and don't want to install the older version.
I think the WebSocket package is not able to send the event and data on a single channel with a newer version of pusher-php-server.
I can't raise an issue (or blame it) for Pusher SDK because we are just replacing the package and I think the Pusher SDK package is working fine when you use their credentials(ie. you have to create an app on Pusher).
Even if you can check on the WebSocket dashboard i.e http://localhost:8000/laravel-websockets when you send the event it will never catch in the front end. But as soon as you update the Pusher.php file it starts catching an event on the front end.
due to the above reason, as you know the notification are sent to the user on their private channels, So I can't add a mock channel for notification as I did for my event, so notification will never catch by the frontend application.
composer.json
"beyondcode/laravel-websockets": "^1.13",
"pusher/pusher-php-server": "^7.2",
"laravel/framework": "^9.19",
package.json
"pusher-js": "^7.5.0",
"laravel-echo": "^1.14.2",
I tried the explicit way as well i.e using the pusher SDK's functions[which are giving 200 status code] but not working. As soon as I do it the dirty way it starts working, I mean everything starts working without any issue.
public function pusherTesting(Request $request)
{
$path = "/apps/123456/events";
$settings = [
'scheme' => 'http',
'port' => '6001',
'path' => '',
'timeout' => '30',
'auth_key' => '1b5d6e5b1ab73b',
'secret' => '3739db6a99c1ba',
'app_id' => '123456',
'base_path' => '/apps/123456',
'host' => '127.0.0.1',
];
$params = [];
$body = '{"name":"Illuminate\\Notifications\\Events\\BroadcastNotificationCreated","data":"{\"site_id\":1,\"domain_url\":\"yucentipede-tuvo.blr3.instawp-testing.xyz\",\"save\":\"socket\",\"id\":\"2f53aac0-8d83-45f4-962d-516c1c8bc97c\",\"type\":\"App\\\\Notifications\\\\SiteDeletedNotification\"}","channels":["private-App.Models.User.7"]}';
$params['body_md5'] = md5($body);
$params_with_signature = \Pusher\Pusher::build_auth_query_params(
$settings['auth_key'],
$settings['secret'],
'POST',
$path,
$params
);
$headers = [
'Content-Type' => 'application/json',
'X-Pusher-Library' => 'pusher-http-php 7.2.1'
];
$client = new \GuzzleHttp\Client();
try {
$response = $client->post(ltrim($path, '/'), [
'query' => $params_with_signature,
'body' => $body,
'http_errors' => false,
'headers' => $headers,
'base_uri' => 'http://127.0.0.1:6001'
]);
} catch (Exception $e) {
print_r($e->getMessage());
}
$response_body = json_decode($response->getBody(), false, 512, JSON_THROW_ON_ERROR);
echo $status = $response->getStatusCode();
die;
}
We're using Symfony Messenger in a Symfony 5 project to integrate with RabbitMQ. It works fine when sending messages within Symfony, but I need the ability to use the Messenger component to send messages from some legacy PHP applications that are not built with the Symfony framework.
Under Symfony, it handles all the magic by injecting the MessageBusInterface and all I need to do is something like this:
public function processTestMessage(MessageBusInterface $bus)
{
$bus->dispatch(new TestMessage('Hello World!');
}
I need to somehow instantiate my own version of $bus that will send AMQP messages the same way that Symfony does. I've been trying to recreate everything that Symfony does behind the scenes to accomplish this, but have not been able to put all the details together.
The crux of the problem is to create my own SendMessageMiddleware that does the same thing as Symfony. After that, it's simple:
$sendersLocator = ???
$eventDispatcher = ???
$sendMessageMiddleware = new($sendersLocator, $eventDispatcher);
$bus = new MessageBus([$sendMessageMiddleware]);
Does anyone have any examples of working code that uses the Messenger component to send AMQP messages outside of Symfony?
This can be improved but it works for me:
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpSender;
use Symfony\Component\Messenger\Bridge\Amqp\Transport\Connection;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBus;
use Symfony\Component\Messenger\Middleware\SendMessageMiddleware;
use Symfony\Component\Messenger\Transport\Sender\SendersLocatorInterface;
$sendersLocator = new class implements SendersLocatorInterface {
public function getSenders(Envelope $envelope): iterable
{
$connection = new Connection(
[
'hosts' => 'localhost',
'port' => 5672,
'vhosts' => '/',
'login' => 'guest',
'password' => 'guest'
],
[
'name' => 'messages'
],
[
'messages' => []
]
);
return [
'async' => new AmqpSender($connection)
];
}
};
$middleware = new SendMessageMiddleware($sendersLocator);
$bus = new MessageBus([$middleware]);
$bus->dispatch(new MyMessage());
I modified the above answer to let me pass the RabbitMQ credentials as an environment variable. This is what I needed for my application. I was trying to write my own DSN parser and discovered that Symfony already does it, so I basically lifted the code from there.
If the environment variable is not set, it defaults to use the same settings shown in the example above.
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpSender;
use Symfony\Component\Messenger\Bridge\Amqp\Transport\Connection;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBus;
use Symfony\Component\Messenger\Middleware\SendMessageMiddleware;
use Symfony\Component\Messenger\Transport\Sender\SendersLocatorInterface;
$sendersLocator = new class implements SendersLocatorInterface {
public function getSenders(Envelope $envelope): iterable
{
$dsn = getenv('MESSENGER_TRANSPORT_DSN') ?: $_ENV['MESSENGER_TRANSPORT_DSN'];
$connection = Connection::fromDsn($dsn);
return [
'async' => new AmqpSender($connection)
];
}
};
$middleware = new SendMessageMiddleware($sendersLocator);
$bus = new MessageBus([$middleware]);
$bus->dispatch(new MyMessage());
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.
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!
I'm using deboer/data-import bundle in Symfony 2.8 to try to import data from csv file to database.
use Doctrine\ORM\EntityManager;
use Ddeboer\DataImport\Workflow;
use Ddeboer\DataImport\Reader\CsvReader;
use Ddeboer\DataImport\Writer\DoctrineWriter;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class CsvFileWriter extends Controller {
private $em;
public function __construct(EntityManager $em) {
$this->em = $em;
}
public function csvImport($csvFile, $entity, $user ,$fileRecord) {
$file = new \SplFileObject($csvFile);
$csvReader = new CsvReader($file);
$csvReader->setHeaderRowNumber(0);
$csvReader->setStrict(false);
$csvReader->setColumnHeaders($headers->csvReaderArray());
$workflow = new Workflow($csvReader);
}}
but keep getting error on line with forkflow:
Error: Cannot instantiate interface Ddeboer\DataImport\Workflow
Output from csvReader:
Array ( [routeNo] => 1 [tripNo] => 1 [callTripPosition] => 1 [depotId] => 9002)
Any suggestions much appreciatd.
Workflow became an interface and StepAggregator implements that interface.
The docs are not updated but you only need to replace the following:
use Ddeboer\DataImport\Workflow;
//...
$workflow = new Workflow($csvReader);
with:
use Ddeboer\DataImport\Workflow\StepAggregator;
//...
$workflow = new StepAggregator($csvReader);
After some research and code debbuging it seems that Ddeboer-import has not updated manual. Error occurs as workflow method became interface only.
This project is not developed anymore so the documentaion is obsolete: they are working on a replacement https://github.com/portphp/portphp which, as they say, is a data import/export workflow for PHP http://portphp.org
Also a symfony bundle is planned but as of today they didn't start on it:
https://github.com/portphp/symfony-bundle
So your only help is search the github repo for the code you need and check the tests files for examples.