I've set up websockets by this manual:
http://socketo.me/docs/hello-world
And it's work.
But now I need to send messages to clients from php, and I don't know how to do this.
I've found some manuals with rabbitMQ which I use in my project? like this:
https://github.com/ratchetphp/Ratchet/issues/659
but I can't understand how to use it.
Maybe someone knows?
You can create Symfony console command to start RabbitMQ consumer, start WebSocket server and send messages to a client once ones received from queue.
protected function execute(InputInterface $input, OutputInterface $output): int
{
$loop = LoopFactory::create();
$pusher = new MessageHandler();
$queueAsyncClient = new AsyncClient($loop, [
"host" => $_ENV['RABBITMQ_HOST'],
"port" => $_ENV['RABBITMQ_PORT'],
"vhost" => $_ENV['RABBITMQ_VHOST'],
"user" => $_ENV['RABBITMQ_USERNAME'],
"password" => $_ENV['RABBITMQ_PASSWORD'],
]);
$connect = $queueAsyncClient->connect();
// When client has connected, retrieve channel (as promise)
$connect->then(function (AsyncClient $client) {
return $client->channel();
// Then declare the queue and exchange
})->then(function (Channel $channel) {
// These method calls all return promises, so we need to combine them
return \React\Promise\all([
$channel,
// Create the queue we'll be using
$channel->queueDeclare($_ENV['RABBITMQ_QUEUE'], false, true),
// Declare an exchange
$channel->exchangeDeclare($_ENV['RABBITMQ_EXCHANGE'], 'direct', false, true),
// Bind the queue to the exchange
$channel->queueBind($_ENV['RABBITMQ_QUEUE'], $_ENV['RABBITMQ_EXCHANGE']),
]);
// Then, when the exchange is all hooked up, hook up the pusher
})->then(function ($connection) use ($pusher) {
/** #var Channel $channel (see first section of all() promise above) */
$channel = $connection[0];
// On messages, consume them using the pusher
return $channel->consume(
function (Message $message) use ($pusher, $channel) {
$content = json_decode($message->content, true);
$connections = $pusher->getUserConnections($content["uid"]);
foreach ($connections as $connection) {
$connection->send(json_encode($content["event"]));
}
},
$_ENV['RABBITMQ_QUEUE'],
'',
false,
true // Acknowledges messages
);
})->done();
$webSocketServer = new \React\Socket\TcpServer("tcp://0.0.0.0:" . $_ENV['WEBSOCKET_PORT'] . "/ws", $loop);
$wsServer = new WsServer($pusher);
$wsServer->enableKeepAlive($loop, 30);
$ioServer = new IoServer(
new HttpServer($wsServer),
$webSocketServer
);
$loop->run();
}
MessageHandler class (like Pusher from the Github issue) basically it stores all connections and handles connection open/close and message events.
class MessageHandler implements MessageComponentInterface
{
protected $connections;
public function __construct()
{
$this->connections = new SplObjectStorage;
}
public function onOpen(ConnectionInterface $conn)
{
$this->connections->attach($conn);
}
public function onMessage(ConnectionInterface $from, $msg)
{
try {
$content = json_decode($msg, true);
if ($content || !isset($content["uid"])) {
$this->connections->rewind();
while ($this->connections->valid()) {
if ($this->connections->current() === $from) {
$this->connections->setInfo($content["uid"]);
}
$this->connections->next();
}
} else {
echo "Invalid message content: " . $msg;
}
} catch (\Throwable $t) {
echo $t->getMessage();
}
}
public function onClose(ConnectionInterface $conn)
{
$this->connections->detach($conn);
}
public function onError(ConnectionInterface $conn, Exception $e)
{
$this->connections->detach($conn);
$conn->close();
}
/**
* #param string|null $uid
* #return ConnectionInterface[]|null
*/
public function getUserConnections($uid)
{
$connections = [];
$this->connections->rewind();
while ($this->connections->valid()) {
if ($this->connections->getInfo() == $uid) {
$connections[] = $this->connections->current();
}
$this->connections->next();
}
return $connections;
}
/**
* #return ConnectionInterface[]|null
*/
public function allConnections()
{
$connections = [];
$this->connections->rewind();
while ($this->connections->valid()) {
$connections[] = $this->connections->current();
$this->connections->next();
}
return $connections;
}
}
Related
Here is the method to test.
public function performPoolRequest(RecreationRequestsCollection $requests): RecreatedPaymentsPoolReading
{
$count_request = count($requests);
if($count_request) {
$pool_recreate_payments = new RecreatedPaymentsPool();
$this->http_client->sendPoolRequest(
$this->generateRequests($requests),
$count_request,
function (ResponseInterface $response, $index) use ($requests, $pool_recreate_payments) {
$this->successHandler($response, $requests, $index, $pool_recreate_payments);
},
function (BadResponseException $reason, $index) use ($requests, $pool_recreate_payments) {
$this->failureHandler($reason, $requests, $index, $pool_recreate_payments);
}
);
return $pool_recreate_payments;
} else {
throw new PoolRequestException('Incorrect amount of requests: ' . $count_request);
}
}
Difficulties arose with this piece of code:
$this->http_client->sendPoolRequest(
$this->generateRequests($requests),
$count_request,
function (ResponseInterface $response, $index) use ($requests, $pool_recreate_payments) {
$this->successHandler($response, $requests, $index, $pool_recreate_payments);
},
function (BadResponseException $reason, $index) use ($requests, $pool_recreate_payments) {
$this->failureHandler($reason, $requests, $index, $pool_recreate_payments);
}
);
I made a mock object $this->http_client
But I don't know how to test methods in the argumets ($this->successHandler, $this->failureHandler, this->generateRequests($requests)) that are sent to the method sendPoolRequest.
All of these methods are protected. I understand how to test them using a reflection object, but I want to know if there is an option to test them within a single test by checking the values in $ pool_recreate_payments.
You have to fake what the http client is doing in some way. That means: actually calling the callback functions. While it might be possible to do that with a PHPUnit mock object, writing own test doubles is often easier.
Here are a few examples to get you started.
public function testAllFailing()
{
$client = new class implements HttpClient {
public function sendPoolRequest(array $requests, int $numRequests, callable $successHandler, callable $errorHandler)
{
foreach ($requests as $index => $request) {
$errorHandler(new BadResponseException(/*...*/), $index);
}
}
};
$myService = new MyService($client);
$result = $myService->performPoolRequest(/*...*/);
self::assertEquals(/*...*/, $result);
}
public function testAllSucceeding()
{
$client = new class implements HttpClient {
public function sendPoolRequest(array $requests, int $numRequests, callable $successHandler, callable $errorHandler)
{
foreach ($requests as $index => $request) {
$successHandler(new Response(/*...*/), $index);
}
}
};
$myService = new MyService($client);
$result = $myService->performPoolRequest(/*...*/);
self::assertEquals(/*...*/, $result);
}
public function testEveryOtherFails()
{
$client = new class implements HttpClient {
public function sendPoolRequest(array $requests, int $numRequests, callable $successHandler, callable $errorHandler)
{
foreach ($requests as $index => $request) {
if ($index % 2 === 0) {
$successHandler(new Response(/*...*/), $index);
} else {
$errorHandler(new BadResponseException(/*...*/), $index);
}
}
}
};
$myService = new MyService($client);
$result = $myService->performPoolRequest(/*...*/);
self::assertEquals(/*...*/, $result);
}
Since Guzzle is used as the http client, I decided to use its capabilities in terms of creating stubs for the response object. Here is a link to the documentation Guzzle documentation. The solution turned out to be simple and allowed us to extensively test the business logic of the response.
I'm trying to unit test my service, containing a dependency Finder Component of symfony2(http://symfony.com/doc/current/components/finder.html).
I'm getting :
[Exception] Objects returned by Mock_Finder_91776c5c::getIterator()
must be traversable or implement interface Iterator
The service :
public function getFile($fileName, $path = '/')
{
if($this->wrapper == null || $this->connection == null)
throw new LogicException("call method setFTP first");
// get file on ftp server
$this->connection->open();
$downloadComplete = $this->wrapper->get($this->tmpDir . $fileName, $path . $fileName);
$this->connection->close();
if($downloadComplete == false)
return false; // TODO exception ?
// return file downloaded
$this->finder->files()->in(__DIR__);
foreach ($this->finder as $file) {
return $file;
}
return false; // TODO exception ?
}
And the test
class FtpServiceTest extends \PHPUnit_Framework_TestCase
{
protected $connectionMock;
protected $ftpWrapperMock;
protected $finderMock;
protected function setUp()
{
$this->connectionMock = $this->getConnectionMock();
$this->ftpWrapperMock = $this->getFTPWrapperMock();
$this->finderMock = $this->getFinderMock();
}
protected function tearDown()
{
}
private function getFinderMock()
{
return $this->getMockBuilder(Finder::class)
->disableOriginalConstructor()
->getMock('Iterator');
}
private function getConnectionMock()
{
return $this->getMockBuilder(Connection::class)
->disableOriginalConstructor()
->getMock();
}
private function getFTPWrapperMock()
{
return $this->getMockBuilder(FTPWrapper::class)
->disableOriginalConstructor()
->getMock();
}
// tests
public function testMe()
{
// arrange
$host = 'localhost';
$user = 'user';
$password = '1234';
$filesArray = new ArrayObject(array(''));
$service = new FtpService('var/tmp/');
$service->setFTP($this->connectionMock, $this->ftpWrapperMock);
$service->setFinder($this->finderMock);
$this->connectionMock
->expects($this->once())
->method('open');
$this->ftpWrapperMock
->expects($this->once())
->method('get')
->will($this->returnValue(true));
$this->connectionMock
->expects($this->once())
->method('close');
$this->finderMock
->expects($this->once())
->method('files')
->will($this->returnValue($this->finderMock));
$this->finderMock
->expects($this->once())
->method('in')
->will($this->returnValue($filesArray));
// act
$file = $service->getFile('/file.zip');
// assert
$this->assertInstanceOf(SplFileInfo::class, $file);
}
}
The mocked instance of the Finder class needs to implement/mock the method getIterator. The getMock method does not accept arguments so don't pass the string 'Iterator'—change the code as follows:
private function getFinderMock()
{
return $this->getMockBuilder(Finder::class)
->disableOriginalConstructor()
->getMock();
}
And add the Mocked expectation in the test method, for example:
$this->finderMock->expects($this->once())
->method('getIterator')
->willReturn(new \ArrayObject([$this->createMock(SplFileInfo::class)]));
Hope this helps.
I have this issue where I'm attempting to create a subrequest in Silex and basically forward my parameters to another controllers. Exhibit A is broken below (after attempts to refactor), and Exhibit B, the original version, works:
Exhibit A ($this->app is lost after creating the request):
class EntriesController {
private $app;
private $req;
public function __construct($app, $req) {
$this->app = $app;
$this->req = $req;
}
public function updateAction() {
//...
//$url defined here (eyesore-ingly long, so not shown)
$subRequest = Request::create($url, 'GET', $params, $this->req->cookies->all(), array(), $this->req->server->all());
//$this->app **no longer** exists here
return $this->app->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
}
}
EntriesController instance is created below:
class AppControllerProvider implements ControllerProviderInterface {
public function connect(Application $app) {
$controllers = $app['controllers_factory'];
//...
$controllers->patch('/edit', function (Request $req) use ($app) {
$entriesCtrl = new \EntriesController($app, $req);
return $entriesCtrl->updateAction();
});
//...
}
Exhibit B (works just fine):
class AppControllerProvider implements ControllerProviderInterface {
public function connect(Application $app) {
$controllers = $app['controllers_factory'];
$controllers->patch('/edit', function (Request $req) use ($app) {
//...
//$url defined here
$subRequest = Request::create($url, 'GET', $params, $this->req->cookies->all(), array(), $this->req->server->all());
return $this->app->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
}
//...
});
I basically just reorganized the logic from Exhibit B's 'PATCH' /edit method body into a controller class, and I passed the Silex Application instance $app to a new instance of the controller class.
The only difference between Exhibit A and Exhibit B as far as I can tell is that you instantiate a controller object in the path method callback. Maybe there is something wrong with how this controller is setup or a namespace issue? Shootin' in the dark here.
I can confirm that in my silex application the following code does not produce a null $app container:
GlobalControllerProvider.php
<?php
namespace Dev\Pub\Provider\Controller;
use Silex\Application;
use Silex\ControllerProviderInterface;
use Symfony\Component\HttpFoundation\Request;
class GlobalControllerProvider implements ControllerProviderInterface
{
public function connect(Application $app)
{
$controllers = $app['controllers_factory'];
$controllers
->get('/', 'Dev\Pub\Controller\GlobalController::indexAction')
->bind('homepage')
;
$controllers
->patch('/edit', function (Request $req) use ($app) {
$entriesCtrl = new \Dev\Pub\Controller\GlobalController();
return $entriesCtrl->updateAction($app, $req);
});
return $controllers;
}
}
GlobalController.php
<?php
namespace Dev\Pub\Controller;
use Silex\Application;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class GlobalController
{
public function indexAction(Application $app, Request $request)
{
return new Response($app['twig']->render('index.html.twig'));
}
public function updateAction(Application $app, Request $request)
{
$url = 'http://silex.local/index_dev.php/';
$params = array();
$subRequest = Request::create($url, 'GET', $params, $request->cookies->all(), array(), $request->server->all());
// outputs: 'Silex\Application'
error_log(print_r(get_class($app),1).' '.__FILE__.' '.__LINE__,0);
// outputs: 1
error_log(print_r(is_object($app),1).' '.__FILE__.' '.__LINE__,0);
return $app->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
}
}
index.main.js
$(function(){
console.log('index.main.js');
$.ajax({
url: "http://silex.local/index_dev.php/edit",
method: "PATCH"
}).done(function( data ) {
console.log(data);
}).fail(function( data ) {
console.log(data);
});
});
I'm trying to create a custom batch action using Sonata Admin Bundle, that sends an email.
The problem is, I'm not sure how to access swiftmailer. I have the following:
public function batchActionSend(ProxyQueryInterface $selectedModelQuery)
{
if($this->admin->isGranted('EDIT')=== false) {
throw new AccessDeniedException();
}
$request = $this->get('request');
$modelManager = $this->admin->getModelManager();
$selectedModels = $selectedModelQuery->execute();
try {
foreach ($selectedModels as $selectedModel) {
// send the email here?
$selectedModel->send();
$modelManager->update($selectedModel);
}
} catch (\Exception $e) {
$this->get('session')->getFlashBag()->add('sonata_flash_error', $e->getMessage());
return new RedirectResponse($this->admin->generateUrl('list',$this->admin->getFilterParameters()));
}
$this->get('session')->getFlashBag()->add('sonata_flash_success', sprintf('The selected requests have been sent'));
return new RedirectResponse($this->admin->generateUrl('list',$this->admin->getFilterParameters()));
In my entity I have the following:
public function send()
{
// send email here?
}
What is the best way to access the mailer? via the Admin Class or via the Entity or via a service?
Thanks
Use Swift_Message::newInstance()
public function send()
{
$message = \Swift_Message::newInstance()
->setSubject('Hello Email')
->setFrom('send#example.com')
->setTo('recipient#example.com')
->setBody(
$this->renderView(
'HelloBundle:Hello:email.txt.twig',
array('name' => $name)
)
)
;
$this->get('mailer')->send($message);
}
I've been searching and haven't really found much useful information. I'm looking to use the symfony2 routing component (and also yaml component) with my own personal framework but haven't found any documentation on basic setup. The documentation on the symfony site does a good job of explaining how to use the component but not much in the way of standalone setup.
Can anyone recommend a good place to start that could explain how to setup individual symfony2 components?
I ended up looking at the documentation on the symfony site, as well as the source code on github (Route.php, RouteCollection.php, RouteCompiler.php) and came up with my own basic router.
Just like symfony, I allow 4 arguments to be passed to the route class -> 1) the pattern, 2) the default controller/action, 3) regex requirements for each variable in the pattern, and 4) any options - I haven't coded for this yet as it's not in my requirements.
My main/front controller (index.php) only talks to the static Router class which has access to the other route classes in the lib\route namespace.
Router::Map basically compiles a regex string (along with default controller/action and variable names) that gets passed to the compiledRouteCollection which is used in Router::_Process to match the httpRequest against. From there, if there is a match in the process method then i set the controller/action/args based on the values of the matched compiledRoute default controller/action. If there is not a preg_match then i use the default controller/action/args from the request object. The rest is cake...
I hope someone finds this useful and it's able to save them the time I spent today working on this. Cheers!
index.php
require_once 'config/config.php';
require_once 'lib/autoload.php';
lib\Router::Map(
'users_username_id',
'users/{username}/{id}',
array('controller' => 'testController', 'action' => 'users')
);
lib\Router::Route(new lib\Request());
router.php
namespace lib;
class Router {
protected static $_controller;
protected static $_action;
protected static $_args;
protected static $_compiledRouteCollection;
private static $_instance;
private function __construct() {
self::$_compiledRouteCollection = new \lib\route\CompiledRouteCollection();
}
public static function Singleton() {
if(!isset(self::$_instance)) {
$className = __CLASS__;
self::$_instance = new $className;
}
return self::$_instance;
}
public static function Route(Request $request, $resetProperties = false) {
self::Singleton();
self::_Process($request,$resetProperties);
$className = '\\app\\controllers\\' . self::$_controller;
if(class_exists($className, true)) {
self::$_controller = new $className;
if(is_callable(array(self::$_controller, self::$_action))) {
if(!empty(self::$_args)) {
call_user_func_array(array(self::$_controller, self::$_action), self::$_args);
} else {
call_user_func(array(self::$_controller, self::$_action));
}
return;
}
}
self::Route(new \lib\Request('error'),true);
}
public static function Map($name, $pattern, array $defaults, array $requirements = array(), array $options = array()) {
self::Singleton();
$route = new \lib\route\Route($pattern,$defaults,$requirements,$options);
$compiledRoute = $route->Compile();
self::$_compiledRouteCollection->Add($name,$compiledRoute);
}
private static function _Process(Request $request, $resetProperties = false) {
$routes = array();
$routes = self::$_compiledRouteCollection->routes;
foreach($routes as $route) {
preg_match($route[0]['regex'], $request->GetRequest(), $matches);
if(count($matches)) {
array_shift($matches);
$args = array();
foreach($route[0]['variables'] as $variable) {
$args[$variable] = array_shift($matches);
}
self::$_controller = (!isset(self::$_controller) || $resetProperties) ? $route[0]['defaults']['controller'] : self::$_controller;
self::$_action = (!isset(self::$_action) || $resetProperties) ? $route[0]['defaults']['action'] : self::$_action;
self::$_args = (!isset(self::$_args) || $resetProperties) ? $args : self::$_args;
return;
}
}
self::$_controller = (!isset(self::$_controller) || $resetProperties) ? $request->GetController() : self::$_controller;
self::$_action = (!isset(self::$_action) || $resetProperties) ? $request->GetAction() : self::$_action;
self::$_args = (!isset(self::$_args) || $resetProperties) ? $request->GetArgs() : self::$_args;
}
}
request.php
namespace lib;
class Request {
protected $_controller;
protected $_action;
protected $_args;
protected $_request;
public function __construct($urlPath = null) {
$this->_request = $urlPath !== null ? $urlPath : $_SERVER['REQUEST_URI'];
$parts = explode('/', $urlPath !== null ? $urlPath : $_SERVER['REQUEST_URI']);
$parts = array_filter($parts);
$this->_controller = (($c = array_shift($parts)) ? $c : 'index').'Controller';
$this->_action = ($c = array_shift($parts)) ? $c : 'index';
$this->_args = (isset($parts[0])) ? $parts : array();
}
public function GetRequest() {
return $this->_request;
}
public function GetController() {
return $this->_controller;
}
public function GetAction() {
return $this->_action;
}
public function GetArgs() {
return $this->_args;
}
}
route.php
namespace lib\route;
class Route {
private $_pattern;
private $_defaults;
private $_requirements;
public $_options;
public function __construct($pattern, array $defaults = array(), array $requirements = array(), array $options = array()) {
$this->SetPattern($pattern);
$this->SetDefaults($defaults);
$this->SetRequirements($requirements);
$this->SetOptions($options);
}
public function SetPattern($pattern) {
$this->_pattern = trim($pattern);
if($this->_pattern[0] !== '/' || empty($this->_pattern)) {
$this->_pattern = '/'.$this->_pattern;
}
}
public function GetPattern() {
return $this->_pattern;
}
public function SetDefaults(array $defaults) {
$this->_defaults = $defaults;
}
public function GetDefaults() {
return $this->_defaults;
}
public function SetRequirements(array $requirements) {
$this->_requirements = array();
foreach($requirements as $key => $value) {
$this->_requirements[$key] = $this->_SanitizeRequirement($key,$value);
}
}
public function GetRequirements() {
return $this->_requirements;
}
public function SetOptions(array $options) {
$this->_options = array_merge(
array('compiler_class' => 'lib\\route\\RouteCompiler'),
$options
);
}
public function GetOptions() {
return $this->_options;
}
public function GetOption($name) {
return isset($this->_options[$name]) ? $this->_options[$name] : null;
}
private function _SanitizeRequirement($key, $regex) {
if($regex[0] == '^') {
$regex = substr($regex, 1);
}
if(substr($regex, -1) == '$') {
$regex = substr($regex,0,-1);
}
return $regex;
}
public function Compile() {
$className = $this->GetOption('compiler_class');
$routeCompiler = new $className;
$compiledRoute = array();
$compiledRoute = $routeCompiler->Compile($this);
return $compiledRoute;
}
}
routeCompiler.php
namespace lib\route;
class RouteCompiler {
//'#\/tellme\/users\/[\w\d_]+\/[\w\d_]+#'
public function Compile(Route $route) {
$pattern = $route->GetPattern();
$requirements = $route->GetRequirements();
$options = $route->GetOptions();
$defaults = $route->GetDefaults();
$len = strlen($pattern);
$tokens = array();
$variables = array();
$pos = 0;
$regex = '#';
preg_match_all('#.\{([\w\d_]+)\}#', $pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
if(count($matches)) {
foreach($matches as $match) {
if($text = substr($pattern, $pos, $match[0][1] - $pos)) {
$regex .= str_replace('/', '\/', $text).'\/';
}
if($var = $match[1][0]) {
if(isset($requirements[$var])) {
$regex .= '('.$requirements[$var].')\/';
} else {
$regex .= '([\w\d_]+)\/';
}
$variables[] = $match[1][0];
}
$pos = $match[0][1] + strlen($match[0][0]);
}
$regex = rtrim($regex,'\/').'#';
} else {
$regex .= str_replace('/', '\/', $pattern).'#';
}
$tokens[] = array(
'regex' => $regex,
'variables' => $variables,
'defaults' => $defaults
);
return $tokens;
}
}
compiledRouteCollection.php
namespace lib\route;
class CompiledRouteCollection {
public $routes;
public function __construct() {
$this->routes = array();
}
public function Add($name, array $route) {
$this->routes[$name] = $route;
}
}