I'm trying to use a custom class in a worker using amphp but it doesn't seem to be working. The below class is already auto-loaded using composer. Please help me out with this issue. My code is below:
Class (this implements Task as mentioned on their docs):
<?php
namespace Jobs;
require '/home/xxx/vendor/autoload.php';
use Amp\Parallel\Worker\Environment;
use Amp\Parallel\Worker\Task;
class GetMarketJob implements Task {
/**
* #var callable
*/
private $function;
/**
* #var mixed[]
*/
private $args;
public function __construct($function, ...$args) {
$this->function = $function;
$this->args = $args;
}
/**
* {#inheritdoc}
*/
public function run(Environment $environment)
{
if ($this->function instanceof \__PHP_Incomplete_Class) {
throw new \Error('When using a class instance as a callable, the class must be autoloadable');
}
if (\is_array($this->callable) && ($this->callable[0] ?? null) instanceof \__PHP_Incomplete_Class) {
throw new \Error('When using a class instance method as a callable, the class must be autoloadable');
}
if (!\is_callable($this->function)) {
$message = 'User-defined functions must be autoloadable (that is, defined in a file autoloaded by composer)';
if (\is_string($this->function)) {
$message .= \sprintf("; unable to load function '%s'", $this->function);
}
throw new \Error($message);
}
return ($this->function)(...$this->args);
}
public function testMe($url = NULL) {
$test = file_get_contents($url);
return $test;
}
}
File using amphp to assign worker using above class:
<?php
require '/home/xxxx/vendor/autoload.php';
use Jobs\GetMarketJob;
// Example async producer using promisor
use Amp\Parallel\Worker;
use Amp\Promise;
use Amp\Loop;
use Amp\Parallel\Worker\DefaultPool;
use Amp\Parallel\Worker\Task;
use Amp\Parallel\Worker\Environment;
use Amp\Parallel\Worker\TaskFailureError;
use Amp\Parallel\Worker\DefaultWorkerFactory;
Amp\Loop::run(function () {
$factory = new DefaultWorkerFactory();
$worker = $factory->create();
$result = yield $worker->enqueue(new GetMarketJob('testMe', ['https://www.syhtek.com']));
print($result);
$code = yield $worker->shutdown();
\printf("Code: %d\n", $code);
});
running this script gives me the below output:
[26-May-2021 01:23:11 UTC] PHP Fatal error: Uncaught
Amp\Parallel\Worker\TaskFailureError: Uncaught Error in worker with
message "User-defined functions must be autoloadable (that is, defined
in a file autoloaded by composer); unable to load function 'testMe'"
and code "0"; use
Amp\Parallel\Worker\TaskFailureError::getOriginalTrace() for the stack
trace in the worker in
/home/xxxx/vendor/amphp/parallel/lib/Worker/Internal/TaskFailure.php:60
Thank you so much for reading!
The issue here is that you're passing 'testMe' and then check if (!\is_callable($this->function)) {, while it should be if (!\method_exists($this, $this->function)) {.
And return ($this->function)(...$this->args); should be return ($this->{$this->function})(...$this->args); if you're trying to call that method. You might also call that method directly instead of giving it to the constructor.
If everything you do in the worker is an HTTP request, you should look into amphp/http-client instead of amphp/parallel, as non-blocking I/O is much more efficient than several chlid processes with blocking I/O.
Related
I struggle with this problem for a while - and the reason is probably trivial.
Background
I've created parser module for my Yii2 application so I can call it from other places (mobile app, etc.) to get data from various websites. There may be many parser classes, all implementing same interface.
Project structure
...
/modules
\_ parser
\_components
\_parsers
\_SampleParser.php
\_controllers
\_DefaultController.php
\_Parser.php
...
I've removed some code for better readability.
DefaultController.php:
namespace app\modules\parser\controllers;
use Yii;
use yii\web\Controller;
use app\modules\parser\components\parsers;
use app\modules\parser\components\parsers\SampleParser;
/**
* Default controller for the `parser` module
*/
class DefaultController extends Controller
{
private function loadParser($parserName){
return new SampleParser(); // if I leave this here, everything works okay
$className = $parserName.'Parser';
$object = new $className();
if ($object instanceof IParseProvider){
return $object;
}
}
...
public function actionIndex()
{
$url = "http://google.com";
$parser = 'Sample';
$loadedParser = $this->loadParser($parser);
$response = $loadedParser->parse($url);
\Yii::$app->response->format = 'json';
return $response->toJson();
}
...
SampleParser.php:
<?php
namespace app\modules\parser\components\parsers;
use app\modules\parser\models\IParseProvider;
class SampleParser implements IParseProvider {
public function canParse($url){
}
public function parse($url){
}
}
Right now everything works more or less ok, so I guess I'm importing correct namespaces. But when I remove return new SampleParser(); and let the object to be created by string name, it fails with error:
PHP Fatal Error – yii\base\ErrorException
Class 'SampleParser' not found
with highlighted line:
$object = new $className();
What am I doing wrong here? Thanks!
Try again with help of Yii:
private function loadParser($parserName)
{
return \yii\di\Instance::ensure(
'app\modules\parser\components\parsers\\' . $parserName . 'Parser',
IParseProvider::class
);
}
Remember that ensure() throws \yii\base\InvalidConfigException when passed reference is not of the type you expect so you need to catch it at some point.
If you are using PHP < 5.5 instead of IParseProvider::class you can use full class name with it's namespace.
P.S. remove use app\modules\parser\components\parsers; unless you have got class named parsers you want to use.
I am I'm wondering why the Container::getInstance() can return a application class.
For example:
I want to make a hash str, I want to know how they work:
app('hash')->make('password');
and I found the source code in laravel :
vendor/laravel/framework/src/Illuminate/Foundation/helpers.php
if (! function_exists('app')) {
/**
* Get the available container instance.
*
* #param string $make
* #param array $parameters
* #return mixed|\Illuminate\Foundation\Application
*/
function app($make = null, $parameters = [])
{
if (is_null($make)) {
return Container::getInstance();
}
return Container::getInstance()->make($make, $parameters);
}
}
I dont know what the Container::getInstance() will return, then I dd(Container::getInstance()) and I know it will can return an application class, but I dont know how they work.
Maybe I'm a little bit late with my answer, but anyway.
Description is current as of Laravel framework version 5.3.24.
Why calling app(), that then calls Container::getInstance() returns object, instance of Application?
For example, why
Route::get('/', function () {
var_dump(app());
});
outputs:
object(Illuminate\Foundation\Application)
...
Because... And here we have to go step by step though it to understand everything.
User initiates a web request. The request is processed by /public/index.php
/public/index.php contains the following:
$app = require_once __DIR__.'/../bootstrap/app.php';
/bootstrap/app.php has the following lines:
$app = new Illuminate\Foundation\Application(
realpath(__DIR__.'/../')
);
When an $app object is instantiated, the Illuminate\Foundation\Application class constructor is called.
/vendor/laravel/framework/src/Illuminate/Foundation/Application.php
class Application extends Container implements ...
{
// ...
public function __construct($basePath = null)
{
// 5. constructor triggers the following method:
$this->registerBaseBindings();
// ...
}
// ...
protected function registerBaseBindings()
{
// 6. which then triggers the following:
static::setInstance($this);
// 7. Important! $this points to class Application here
// and is passed to Container
// ...
}
// ...
}
static::setInstance($this); refers us to class Container, because class Application extends Container
/vendor/laravel/framework/src/Illuminate/Container/Container.php
class Container implements ...
{
// ...
// 11. $instance now contains an object,
// which is an instance of Application class
protected static $instance;
// ...
public static function setInstance(ContainerContract $container = null)
{
// 9. $container = Application here, because it has been passed
// from class Application while calling static::setInstance($this);
// 10. Thus, static::$instance is set to Application here
return static::$instance = $container;
}
// ...
}
Now, suppose, we have written the following lines in our routes file.
/routes/web.php
Route::get('/', function () {
dd(app()); // 13. We a calling an app() helper function
});
14 Calling app() leads us to
/vendor/laravel/framework/src/Illuminate/Foundation/helpers.php
// ...
/** #return mixed|\Illuminate\Foundation\Application */
function app($make = null, $parameters = [])
{
// 15. $make is null, so this is the case
if (is_null($make)) {
// 16. The following static method is called:
return Container::getInstance();
}
// ...
}
// ...
Now we are back in our Container class
/vendor/laravel/framework/src/Illuminate/Container/Container.php
public static function getInstance()
{
// 18. Important!
// To this point static::$instance is NOT null,
// because it has already been set (up to "step 11").
if (is_null(static::$instance)) {
static::$instance = new static; // Thus, we skip this.
}
// 19. static::$instance is returned
// that contains an object,
// which is an instance of Application class
return static::$instance;
}
Some important notes for steps 16-19.
Important note 1!
Static Keyword
Declaring class properties or methods as static makes them accessible
without needing an instantiation of the class.
Important note 2!
static::$instance = new static; is NOT related to calling our app() function in step 13. And was somewhat misleading for me at first...
But just to note, it makes use of Late Static Bindings.
The Application class (Illuminate\Foundation\Application) extends the Container class. This is the core of the framework and allows all the dependency injection magic, and it follows a Sigleton pattern, this means that when you request the Application object (with the app() helper function, or more internally Container::getInstance()) anywhere in your code you get the same global instance.
Without getting to complex: this let's you bind any class into that Container instance:
app()->bind('MyModule', new MyModuleInstace());
So then you can "resolve" that class out of the container with:
app()->make('MyModule);
But remember there are several methods to do this, with different pattern targets, this is just a basic demonstration of the concept.
I am trying to write unit test for my application. which as logging the information functionality.
To start with i have service called LogInfo, this how my class look like
use Zend\Log\Logger;
class LogInfo {
$logger = new Logger;
return $logger;
}
I have another class which will process data. which is below.
class Processor
{
public $log;
public function processData($file)
{
$this->log = $this->getLoggerObj('data');
$this->log->info("Received File");
}
public function getLoggerObj($logType)
{
return $this->getServiceLocator()->get('Processor\Service\LogInfo')->logger($logType);
}
}
here i am calling service Loginfo and using it and writing information in a file.
now i need to write phpunit for class Processor
below is my unit test cases
class ProcessorTest{
public function setUp() {
$mockLog = $this->getMockBuilder('FileProcessor\Service\LogInfo', array('logger'))->disableOriginalConstructor()->getMock();
$mockLogger = $this->getMockBuilder('Zend\Log\Logger', array('info'))->disableOriginalConstructor()->getMock();
$serviceManager = new ServiceManager();
$serviceManager->setService('FileProcessor\Service\LogInfo', $mockLog);
$serviceManager->setService('Zend\Log\Logger', $mockLogger);
$this->fileProcessor = new Processor();
$this->fileProcessor->setServiceLocator($serviceManager);
}
public function testProcess() {
$data = 'I have data here';
$this->fileProcessor->processData($data);
}
}
I try to run it, i am getting an error "......PHP Fatal error: Call to a member function info() on a non-object in"
i am not sure , how can i mock Zend logger and pass it to class.
Lets check out some of your code first, starting with the actual test class ProcessorTest. This class constructs a new ServiceManager(). This means you are going to have to do this in every test class, which is not efficient (DRY). I would suggest constructing the ServiceMananger like the Zend Framework 2 documentation describes in the headline Bootstrapping your tests. The following code is the method we are interested in.
public static function getServiceManager()
{
return static::$serviceManager;
}
Using this approach makes it possible to obtain the instance of ServiceManager through Bootstrap::getServiceManager(). Lets refactor the test class using this method.
class ProcessorTest
{
protected $serviceManager;
protected $fileProcessor;
public function setUp()
{
$this->serviceManager = Bootstrap::getServiceManager();
$this->serviceManager->setAllowOverride(true);
$fileProcessor = new Processor();
$fileProcessor->setServiceLocator($this->serviceManager);
$this->fileProcessor = $fileProcessor;
}
public function testProcess()
{
$mockLog = $this->getMockBuilder('FileProcessor\Service\LogInfo', array('logger'))
->disableOriginalConstructor()
->getMock();
$mockLogger = $this->getMockBuilder('Zend\Log\Logger', array('info'))
->disableOriginalConstructor()
->getMock();
$serviceManager->setService('FileProcessor\Service\LogInfo', $mockLog);
$serviceManager->setService('Zend\Log\Logger', $mockLogger);
$data = 'I have data here';
$this->fileProcessor->processData($data);
}
}
This method also makes it possible to change expectations on the mock objects per test function. The Processor instance is constructed in ProcessorTest::setUp() which should be possible in this case.
Any way this does not solve your problem yet. I can see Processor::getLoggerObj() asks the ServiceManager for the service 'Processor\Service\LogInfo' but your test class does not set this instance anywhere. Make sure you set this service in your test class like the following example.
$this->serviceManager->setService('Processor\Service\LogInfo', $processor);
In a silex application I have a KafkaAPiClient class which definitely has the public method postMessages.
<?php
namespace Kopernikus\KafkaWriter;
use Kopernikus\KafkaWriter\Model\AbstractMessage;
/**
* KafkaApiClient.
**/
class KafkaApiClient
{
/**
* #param AbstractMessage[] $msg
*/
public function postMessages(array $messages)
{
foreach ($messages as $message) {
$this->postMessage($message);
}
}
public function postMessage(AbstractMessage $msg)
{
...
}
}
I can call KafkaAPiClient::postMessages just fine, yet when mocking the class in a test:
<?php
namespace unit\Request;
use Kopernikus\KafkaWriter\KafkaApiClient;
/**
* MockeryMethodsNotBeingCallableTest
**/
class MockeryMethodsNotBeingCallableTest extends \PHPUnit_Framework_TestCase
{
public function testMockMethodIsCallable()
{
$leMock = \Mockery::mock(KafkaApiClient::class);
$leMock->postMessages([]);
}
}
I am getting:
1) unit\Request\MockeryMethodsNotBeingCallableTest::testMockMethodIsCallable
BadMethodCallException: Method Mockery_11_Kopernikus_KafkaWriter_KafkaApiClient::postMessages() does not exist on this mock object
~/le-project/tests/unit/Request/MockeryMethodsNotBeingCallableTest.php:14
I am confused, I was expecting for the mock to not do anything yet allow the methods to be called so that I later could add my expectations on it.
Though I have found a solution, I am still wondering if it is possible to mock all the methods by default, and later check if certain ones have been called.
There exists shouldIgnoreMissing method on the mock object. Calling that does exactly what it says on the tin, that is: ignoring calls to not yet defined methods, resulting in a mock that does nothing:
$leMock = \Mockery::mock(KafkaApiClient::class);
$leMock->shouldIgnoreMissing()
$leMock->postMessages([]);
And by nothing, it means nothing. I got into an other error for my queue when I instantiated the mock that way, as methods will return null by default and their return value has to be explicitly stated.
$msg = new Message('dummy-message');
$this->kafkaQueue
->shouldIgnoreMissing()
->shouldReceive('getMessages')->andReturn([$msg]);
Any call to getMessages will now return exactly the array [$msg].
Alternatively, one can be very explicit about what methods are called with Mockery, by adding shouldReceive:
public function testMockMethodIsCallable()
{
$leMock = \Mockery::mock(KafkaApiClient::class);
$leMock->shouldReceive('postMessages');
$leMock->postMessages([]);
}
If someone work with GO! framework, can you help me.
I install framework on php 5.3.13. Demo example is working.
But my own example doesn't work. Aspect(method beforeMethodExecution) is not perfomed.
Here is my code.
Main file:
//1 Include kernel and all classes
if (file_exists(__DIR__ .'/../../vendor/autoload.php')) {
$loader = include __DIR__ .'/../../vendor/autoload.php';
}
// 2 Make own ascpect kernel
use Go\Core\AspectKernel;
use Go\Core\AspectContainer;
class Kernel extends AspectKernel{
/**
* Configure an AspectContainer with advisors, aspects and pointcuts
*
* #param AspectContainer $container
*
* #return void
*/
public function configureAop(AspectContainer $container)
{
}
}
//3 Initiate aspect kernel
$Kernel = Kernel::getInstance();
$Kernel->init();
//4 Include aspect
include(__DIR__.'/aspectclass/AspectClass.php');
$aspect = new DebugAspect();
//5 register aspect
$Kernel->getContainer()->registerAspect($aspect);
//6 Include test class
include(__DIR__.'/class/class1.php');
//7 Execute test class
$Class = new General('test');
$Class->publicHello();
File with test class:
class General{
protected $message = '';
public function __construct($message)
{
$this->message = $message;
}
public function publicHello()
{
echo 'Hello, you have a public message: ', $this->message, "<br>", PHP_EOL;
}
}
File with aspect:
use Go\Aop\Aspect;
use Go\Aop\Intercept\FieldAccess;
use Go\Aop\Intercept\FunctionInvocation;
use Go\Aop\Intercept\MethodInvocation;
use Go\Lang\Annotation\After;
use Go\Lang\Annotation\Before;
use Go\Lang\Annotation\Around;
use Go\Lang\Annotation\Pointcut;
use Go\Lang\Annotation\DeclareParents;
use Go\Lang\Annotation\DeclareError;
class DebugAspect implements Aspect{
/**
* Method that should be called before real method
*
* #param MethodInvocation $invocation Invocation
* #Before("execution(General->*(*))")
*
*/
public function beforeMethodExecution(MethodInvocation $invocation)
{
$obj = $invocation->getThis();
echo 'Calling Before Interceptor for method: ',
is_object($obj) ? get_class($obj) : $obj,
$invocation->getMethod()->isStatic() ? '::' : '->',
$invocation->getMethod()->getName(),
'()',
' with arguments: ',
json_encode($invocation->getArguments()),
PHP_EOL;
}
}
As you know, go-aop isn't a PHP extension, so it couldn't transform classes that were loaded directly via require or include. Internally it tries to overwrite the source code on-the-fly, but it should receive a control (via integration with composer or custom autoloader class).
So, you have an error here:
//6 Include test class
include(__DIR__.'/class/class1.php');
You explicitly load this class into memory and there is no way to transform it from userland. To pass a control to the framework, you should make this explicitly. Look at the line AopComposerLoader.php#L99 to have an idea how it works. Here we include a source file via the stream source filter that pass control to the framework and it can transform the class to weave an aspects.
To fix your example just change an include to the following:
include (FilterInjectorTransformer::rewrite(__DIR__.'/class/class1.php'));