Send through object reference from Laravel Job (simple OOP?) - php

I'm trying to pass a newed up Model Object as a reference from a Laravel Job to the called Object and method. I need to new up the particular model in the Job because I need it in the Job's failed() method to update that model (and persist to database) as it functions as a log.
class ScrapeJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $store;
protected $scrape;
public function __construct(Store $store)
{
$this->store = $store;
}
public function handle()
{
$this->scrape = new \App\Scrape; // This is the log object.
// Here I call the SuperStoreScraper
$class = '\App\Scraper\\' . $this->store->scraper;
(new $class($this->store, $this->scrape))->scrape();
}
public function failed(\Exception $e)
{
// Update the Scrape object and persist to database.
$data = $this->scrape->data; // here I get the error... ->data is not found?
$data['exception'] = [ // this should work since I'm casting data to an array in the Model class.
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
];
$this->scrape->data = $data;
$this->scrape->status = 'failed';
$this->scrape->save();
}
}
class SuperStoreScraper extends Scraper
{
public function __construct(Store $store, Scrape &$scrape) {
parent::__construct($store, $scrape);
}
public function scrape() {
$this->start();
}
}
abstract class Scraper
{
protected $store;
protected $scrape;
public function __construct(Store $store, Scrape &$scrape) {
$this->store = $store;
$this->scrape = &$scrape;
}
protected function start()
{
$this->scrape->fill([
'data' => [
'store' => $this->store->name,
'scraper' => $this->store->scraper
],
'status' => 'scraping'
])->save();
}
}
Everything seems to work fine. The newed up object is passed to both the SuperStoreScraper and the parent Scraper class (through the constructor), BUT when I persist it to the database in the Scraper object start() method, it doesn't reflect that up to the ScrapeJob (which newed up the Scrape object) and then I get an error in the Job's failed() method when trying to update the persisted object.
ErrorException: Trying to get property of non-object in ...\app\Jobs\ScrapeJob.php:54
What am I missing here?

I think I have kind of solved my issue now.
I've moved the \App\Scrape instantiation to the Job's __constructor, but also persist it to the database, like this:
protected $scrape;
public function __construct(Store $store)
{
$this->store = $store;
$this->scrape = \App\Scrape::create(['status' => 'queued']);
}
This works, I can access the Scrape model from both the Scraper and the Job's failed() method, BUT, if I have multiple tries on the Job I'd like to create new Scrape instantiations for each try. Right now the same Scrape instance (same id) is just updated with each Job retry.

Related

How to access route parameters from a service tagged monolog.processor?

Using Symfony 4.4, I'd like to add on the fly to all my a route parameter if it's found in the on request. E.g. for the user Id:
PUT | DELETE | POST mywebsite.com/users/{userid}/some-action
I could add my users ids each time I log but it's kind of cumbersome.
So I created a service:
//src/
// |__Services/
// |___UserProcessor.php
use Monolog\Processor\ProcessorInterface;
use Symfony\Component\HttpFoundation\RequestStack;
// In my config.yaml this monolog.processor
final class UserProcessor implements ProcessorInterface
{
private RequestStack $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function __invoke(array $record): array
{
dd($this->requestStack->getMasterRequest()->attributes);
}
}
When I run my postman on POST /users/{userid}/some-action I get this output:
[
"media_type" => "application/json"
]
From my understanding, symfony route parameters request attributes are not built yet at the moment my processor runs.
What should I do to make my processor access the attribute userid?
Check how the Symfony\Bridge\Monolog\Processor\RouteProcessor does it:
It's implemented as an EventSubscriber
class RouteProcessor implements EventSubscriberInterface, ResetInterface
{
private $routeData;
private $includeParams;
public function __construct(bool $includeParams = true)
{
$this->includeParams = $includeParams;
$this->reset();
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => ['addRouteData', 1],
KernelEvents::FINISH_REQUEST => ['removeRouteData', 1],
];
}
// rest of the implementation
}
During KernelEvents::REQUEST it adds the required request data (including request parameters, if so desired) to the internal state of the object, so when the processor is run it can access the data from the internal RouteProcessor::$routeData, and not from the request directly.
public function addRouteData(RequestEvent $event)
{
if ($event->isMainRequest()) {
$this->reset();
}
$request = $event->getRequest();
if (!$request->attributes->has('_controller')) {
return;
}
$currentRequestData = [
'controller' => $request->attributes->get('_controller'),
'route' => $request->attributes->get('_route'),
];
if ($this->includeParams) {
$currentRequestData['route_params'] = $request->attributes->get('_route_params');
}
$this->routeData[spl_object_id($request)] = $currentRequestData;
}
You could modify this approach to suit yourself, or even just use this processor directly (although it adds more data than what you are looking for in your question).

"Call to a member function updateJob() on null"

Now, I know this question has been asked a lot, but I searched and searched but I just can't figure it out and I've been stuck for hours now. I'm really sorry if it turnes out to be a really dumb mistake (It probably will).
So, I have my Controller which instatiates the editAction() function when a button on my website is pressed. It checks if the request is a _POST request, passes on the data and checks if the input given is valid, all this works fine.
I then try to access a function in my Manager class. And that's where the error is happening and my website spits out:
"Call to a member function updateJob() on null".
Now, PhpStorm is not saying there's an error or a warning, it recognizes the jobManager class and I've checked the namespaces and class names, but all are correct. The variables are also defined correctly, as far as I can see. I'd be really thankful if someone could tell me what I am doing wrong. The code is below.
current state after adding $jobManager to __construct:
class IndexController extends AbstractActionController
{
/**
* Entity manager.
* #var EntityManager
*/
private $entityManager;
/**
* Post manager.
* #var JobManager
*/
private $jobManager;
public function __construct($entityManager, $jobManager)
{
$this->entityManager = $entityManager;
/***
* Edit from comment advice:
* I have added this line to my __construct
* But this does not solve the issue.
***/
$this->jobManager = $jobManager;
}
public function indexAction()
{
// Get recent jobs
$jobs = $this->entityManager->getRepository(Jobs::class)
->findBy(['status'=>Jobs::STATUS_READY]
);
// Render the view template
return new ViewModel([
'jobs' => $jobs
]);
}
public function editAction()
{
// Create the form.
$form = new JobForm();
// Get post ID.
$jobId = $this->params()->fromRoute('id', -1);
// Find existing job in the database.
$jobs = $this->entityManager->getRepository(Jobs::class)
->findOneById($jobId);
if ($jobs == null) {
$this->getResponse()->setStatusCode(404);
return;
}
// Check whether this job is a POST request.
if ($this->getRequest()->isPost()) {
// Get POST data.
$data = $this->params()->fromPost();
// Fill form with data.
$form->setData($data);
if ($form->isValid()) {
// Get validated form data.
$data = $form->getData();
// Use job manager service to add new post to database.
$this->jobManager->updateJob( $jobs, $data);
// Redirect the user to "backups" page.
return $this->redirect()->toRoute('backups');
}
} else {
$data = [
'id' => $jobs->getId(),
'jobName' => $jobs->getJobName(),
'status' => $jobs->getStatus(),
'vmId' => $jobs->getVmId(),
'targetfolderPrefix' => $jobs->getTargetFolderPrefix(),
'numberOfBackups' => $jobs->getNumberOfBackups(),
'lastBackupUsed' => $jobs->getLastBackupUsed(),
'repeat' => $jobs->getRepeat(),
'scheduleRunAtMinute' => $jobs->getScheduleRunAtMinute(),
'scheduleRunAtHour' => $jobs->getScheduleRunAtHour(),
'scheduleRunAtDOW' => $jobs->getScheduleRunAtDOW(),
'hostId' => $jobs->getHostId()
];
$form->setData($data);
}
// Render the view template.
return new ViewModel([
'form' => $form,
'jobs' => $jobs
]);
}
}
What is wrong
$this->jobManager->updateJob( $jobs, $data);
You are telling PHP:
"In $this class, look in the jobManager object and run the method updateJob with these variables.... "
But in $this class you have written:
/**
* Post manager.
* #var JobManager
*/
private $jobManager;
But you have nowhere set jobManager to be anything. You have no setter function in the class as well as no other function setting what a jobManager variable actually is... so jobManager can never be anything.
So what you're in effect doing is saying to PHP
"In $this class, look in the jobManager empty null-space and run the method updateJob with these variables..."
This is clearly not going to end well.
How to fix it
You need to set what jobManager is before you can use it, as referenced by Xatenev. Typically when you construct the class or using a Setter method, if preferred.
ONE:
public function __construct(EntityManager $entityManager, JobManager $jobManagerVar)
{
$this->entityManager = $entityManager;
$this->jobManager = $jobManagerVar;
}
Alternatively - if ->jobManager method needs to be defined after the object IndexController is created; then you need to use a Setter class (because the jobManager var is *private*.
Thus TWO:
class IndexController extends AbstractActionController
{
...
public function setJobManager($jobManagerVar){
$this->jobManager = $jobManagerVar
}
...
}
And then when you instantiate the IndexController you can do:
// ONE from above:
$theClass = new IndexController($entity,$jobManager);
or
// TWO from above
$theClass = new IndexController($entity);
...
$theClass->setJobManager($jobManger);
There are various other nuances as to methods of setting values in classes, I'm not going to go over them all, it will depend on what's going on in your wider project.

Mocking class parameter that returns a mock

I am new to unit testing and trying to test a controller method in Laravel 5.1 and Mockery.
I am trying to test a registerEmail method I wrote, below:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Response;
use Mailchimp;
use Validator;
/**
* Class ApiController
* #package App\Http\Controllers
*/
class ApiController extends Controller
{
protected $mailchimpListId = null;
protected $mailchimp = null;
public function __construct(Mailchimp $mailchimp)
{
$this->mailchimp = $mailchimp;
$this->mailchimpListId = env('MAILCHIMP_LIST_ID');
}
/**
* #param Request $request
* #return \Illuminate\Http\JsonResponse
*/
public function registerEmail(Request $request)
{
$this->validate($request, [
'email' => 'required|email',
]);
$email = $request->get('email');
try {
$subscribed = $this->mailchimp->lists->subscribe($this->mailchimpListId, [ 'email' => $email ]);
//var_dump($subscribed);
} catch (\Mailchimp_List_AlreadySubscribed $e) {
return Response::json([ 'mailchimpListAlreadySubscribed' => $e->getMessage() ], 422);
} catch (\Mailchimp_Error $e) {
return Response::json([ 'mailchimpError' => $e->getMessage() ], 422);
}
return Response::json([ 'success' => true ]);
}
}
I am attempting to mock the Mailchimp object to work in this situation.
So far, my test looks as follows:
<?php
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class HomeRouteTest extends TestCase
{
use WithoutMiddleware;
public function testMailchimpReturnsDuplicate() {
$listMock = Mockery::mock('Mailchimp_Lists')
->shouldReceive('subscribe')
->once()
->andThrow(\Mailchimp_List_AlreadySubscribed::class);
$mailchimp = Mockery::mock('Mailchimp')->lists = $listMock;
$this->post('/api/register-email', ['email'=>'duplicate#email.com'])->assertJson(
'{"mailchimpListAlreadySubscribed": "duplicate#email.com is already subscribed to the list."}'
);
}
}
I have phpUnit returning a failed test.
HomeRouteTest::testMailchimpReturnsDuplicate
Mockery\Exception\InvalidCountException: Method subscribe() from Mockery_0_Mailchimp_Lists should be called exactly 1 times but called 0 times.
Also, if I assert the status code is 422, phpUnit reports it is receiving a status code 200.
It works fine when I test it manually, but I imagine I am overlooking something fairly easy.
I managed to solve it myself. I eventually moved the subscribe into a seperate Job class, and was able to test that be redefining the Mailchimp class in the test file.
class Mailchimp {
public $lists;
public function __construct($lists) {
$this->lists = $lists;
}
}
class Mailchimp_List_AlreadySubscribed extends Exception {}
And one test
public function testSubscribeToMailchimp() {
// create job
$subscriber = factory(App\Models\Subscriber::class)->create();
$job = new App\Jobs\SubscribeToList($subscriber);
// set up Mailchimp mock
$lists = Mockery::mock()
->shouldReceive('subscribe')
->once()
->andReturn(true)
->getMock();
$mailchimp = new Mailchimp($lists);
// handle job
$job->handle($mailchimp);
// subscriber should be marked subscribed
$this->assertTrue($subscriber->subscribed);
}
Mockery will expect the class being passed in to the controller be a mock object as you can see here in their docs:
class Temperature
{
public function __construct($service)
{
$this->_service = $service;
}
}
Unit Test
$service = m::mock('service');
$service->shouldReceive('readTemp')->times(3)->andReturn(10, 12, 14);
$temperature = new Temperature($service);
In laravel IoC it autoloads the classes and injects them, but since its not autoloading Mailchimp_Lists class it won't be a mock object. Mailchimp is requiring the class atop it's main class require_once 'Mailchimp/Lists.php';
Then Mailchimp is then loading the class automatically in the constructor
$this->lists = new Mailchimp_Lists($this);
I don't think you'll be able to mock that class very easily out of the box. Since there isn't away to pass in the mock object to Mailchimp class and have it replace the instance of the real Mailchimp_Lists
I see you are trying to overwrite the lists member variable with a new Mock before you call the controller. Are you certain that the lists object is being replaced with you mocked one? Try seeing what the classes are in the controller when it gets loaded and see if it is in fact getting overridden.

Can't initialize my plugin function in ZF2 constructor

I am quite new to ZF2 and I am preparing a demo application with simple login and CRUD system. Now for login I have prepared a plugin which consists of some functions that will authenticate users, return the logged in user data, return the logged in status etc. But the problem that I am facing is I can't initialize any variable into the constructor of my controller which will store any return value from the plugin. It's always showing service not found exception.
Please find my plugin code below:
AuthenticationPlugin.php
<?php
namespace Album\Controller\Plugin;
use Zend\Mvc\Controller\Plugin\AbstractPlugin;
use Zend\Session\Container as SessionContainer;
use Zend\View\Model\ViewModel;
use Album\Entity\User;
class AuthenticationPlugin extends AbstractPlugin{
protected $entityManager;
protected $usersession;
public function __construct(){
$this->usersession = new SessionContainer('UserSession');
}
public function dologin($email,$password)
{
$getData = $this->em()->getRepository('Album\Entity\User')->findOneBy(array('email' => $email, 'password' => $password));
if(count($getData)){
$this->usersession->offsetSet('userid', $getData->getId());
return true;
}
else{
return false;
}
}
public function isloggedin(){
$userid = $this->usersession->offsetGet('userid');
if(!empty($userid)){
return true;
}
else{
return false;
}
}
public function logindata(){
$userid = $this->usersession->offsetGet('userid');
$getData = $this->em()->getRepository('Album\Entity\User')->findOneBy(array('id' => $userid));
return $getData;
}
public function logout(){
$this->usersession->offsetUnset('userid');
}
public function em(){
return $this->entityManager = $this->getController()->getServiceLocator()->get('Doctrine\ORM\EntityManager');
}
}
?>
In my module.config.php
'controller_plugins' => array(
'invokables' => array(
'AuthPlugin' => 'Album\Controller\Plugin\AuthenticationPlugin',
)
),
Now I am doing this in my controller:
protected $entityManager;
protected $isloggedin;
protected $authentication;
public function __construct(){
$this->authentication = $this->AuthPlugin();
$this->isloggedin = $this->authentication->isloggedin();
}
The error I am getting is like below:
An error occurred An error occurred during execution; please try again
later. Additional information:
Zend\ServiceManager\Exception\ServiceNotFoundException
File:
D:\xampp\htdocs\subhasis\zf2-tutorial\vendor\zendframework\zendframework\library\Zend\ServiceManager\ServiceManager.php:555
Message:
Zend\Mvc\Controller\PluginManager::get was unable to fetch or create an instance for AuthPlugin
But if I write the above constructor code in any of my controller actions everything is fine. in ZF1 I could initialize any variable in the init() method and could use the variable in any of my actions. How can I do this in ZF2? Here, I want to detect if the user is logged in the constructor itself. Now I have to call the plugin in every action which I don't want.
What should I do here?
The error you are receiving is because you are trying to use the ServiceManager (via the Zend\Mvc\Controller\PluginManager) in the __construct method of the controller.
When a controller is registered as an invokable class, the Service Manager (ControllerManager) is responsible for the creating the controller instance. Once created, it will then call the controllers various default 'initializers' which also inlcudes the plugin manager. By having your code in __construct it is trying to use the plugin manager before it has been set.
You can resolve this by using a controller factory, rather than an invokable in module.config.php.
'controllers' => [
'factories' => [
'MyModule\Controller\Foo' => 'MyModule\Controller\FooControllerFactory',
],
],
Then the factory
namespace MyModule\Controller\FooControllerFactory;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class FooControllerFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $controllerManager)
{
$serviceManager = $controllerManager->getServiceLocator();
$controllerPluginManager = $serviceManager->get('ControllerPluginManager');
$authPlugin = $controllerPluginManager->get('AuthPlugin');
return new FooController($authPlugin);
}
}
Lastly, update the controller __construct to add the new argument and remove the call to $this->authPlugin()
class FooController extends AbstractActionController
{
public function __construct(AuthPlugin $authentication)
{
$this->authentication = $authentication;
$this->isloggedin = $authentication->isloggedin();
}
}

testing php laravel controller shouldReceive arguments

I have a laravel model which uses ardent/eloquent. I am trying to set up tests for the controller in particular, storing a new model that uses the ardent model.
The method works in the app but I'm having trouble with my tests
I'm having problems working out how to mock the calls this method makes.
My controllers set up and the method in question is this one:
use golfmanager\service\creator\TicketCreatorInterface;
//controller manages the ticket books
class BooksController extends BaseController {
/**
* Book Repository
*
* #var Book
*/
protected $book;
protected $ticket;
public function __construct(Book $book, TicketCreatorInterface $ticket)
{
$this->book = $book;
$this->ticket = $ticket;
}
public function store()
{
$input = Input::all();
$result = $this->book->save();
if ($result) {
//if book created then create tickets
$this->ticket->createTicket($input, $this->book);
return Redirect::route('books.index');
}
return Redirect::route('books.create')
->withInput()
->withArdentErrors()
->with('message', 'There were validation errors.');
}
And the methods used by the interface (TicketCreator):
public function createTicket($input, $book) {
//dd($input);
$counter = $input['start_number'];
while($counter <= $input['end_number']) {
$ticketDetails = array(
'ticketnumber'=>$counter,
'status'=>'unused',
'active'=>1
);
$this->ticket->create($ticketDetails)->save();
$this->ticket->book()->associate($book)->save();
$counter = $counter+1;
}
return $counter;
}
My test is as follows:
use Mockery as m;
use Way\Tests\Factory;
class BooksTest extends TestCase {
public function __construct()
{
$this->mock = m::mock('Ardent', 'Book');
$this->collection = m::mock('Illuminate\Database\Eloquent\Collection')->shouldDeferMissing();
}
public function setUp()
{
parent::setUp();
$this->attributes = Factory::book(['id' => 1, 'assigned_date'=> '20/11/2013']);
$this->app->instance('Book', $this->mock);
}
public function testStore()
{
Input::replace($input = ['start_number'=>1000, 'end_number'=>1010, 'assigned_date'=>'20/11/2013']);
$this->mock->shouldReceive('save')->once()->andReturn(true);
$this->ticket->shouldReceive('createTicket')->once()->with($input, $this->mock)->andReturn(true);
//with($input);
//$this->validate(true);
$this->call('POST', 'books');
$this->assertRedirectedToRoute('books.index');
}
Currently I get an error:
No matching handler found for Book::save()
Is this being thrown because the book model doesnt contain a save method? If it is how do I mock the model correctly. I don't want it to touch the database (although it could if it has to).
Is it the multiple saves in the createTicket method?
Still learning how to set up tests correctly - slowly getting there but not enough knowledge yet.
If I change the name of the method in shouldReceive to say 'store' it still comes up with the save() error message.
Update:
I have isolated part of the problem to the createTicket call. I've changed my testStore test and updated as above.
My error with this current test is: Undefined index: start_number.
If I remove the call to createTicket in the controller method I don't get an error. I tried using Input::replace to replace the input from a form but appears not getting through to my function
How can I simulate a form input in the mocked objects?
Thanks

Categories