I am running Laravel 5 project and I created a controller in App->Mail->SendEmail to send emails with an attached system-generated invoice through mpdf, with the below code the invoice is generated but downloaded in my browser before being sent, and when changing the output to be s I get the error "Call to a member function output() on null"
/**
* Create a new message instance.
*
* #return void
*/
public function __construct(Order $order, $data, $mpdf)
{
$mpdf = new \Mpdf\Mpdf();
$mpdf->WriteHTML(view('invoices.paid_invoice', compact('order', 'data')));
$fileName = 'Invoice-'.$order->id.'.pdf';
$mpdf->Output($fileName,"D");
$this->order = $order;
}
/**
* Build the message.
*
* #return $this
*/
public function build()
{
return $this->markdown('emails.send_order')
->subject(('my subject'))
->attachData($this->mpdf->output(), 'invoice.pdf', [
'mime' => 'application/pdf',])
;
}}
In your code $this->mpdf references nothing.
You dont need to use MDF anymore once you created the file so just save the filename and reuse that in the build function
private $filename;
/**
* Create a new message instance.
*
* #return void
*/
public function __construct(Order $order, $data, $mpdf)
{
$mpdf = new \Mpdf\Mpdf();
$mpdf->WriteHTML(view('invoices.paid_invoice', compact('order', 'data')));
$this->filename = 'Invoice-'.$order->id.'.pdf';
$mpdf->Output($this->filename,"D");
$this->order = $order;
}
/**
* Build the message.
*
* #return $this
*/
public function build()
{
return $this->markdown('emails.send_order')
->subject(('my subject'))
->attach($this->filename, [
'as' => 'invoice.pdf',
'mime' => 'application/pdf',
]);
}
As #N69S already mentioned in their answer, the mPDF object is not assigned to the class property. The best would be to avoid any "heavy lifting" in the constructor of the class. You need to call the Output method of mPDF exactly once. You can also create the mPDF object in the build method directly (or use a wrapper for Laravel providing a Facade, if that is your poison).
The main issue with provided examples is that with used second parameter 'D', the Output method echoes the PDF content directly (see $dest in Output method documentation) and does not return/save anything. You need either:
To save the output to the $this->filename property (which can also be a local variable only in your example). This is done with $mpdf->Output($this->filename, 'F');,
Or output the binary data of the PDF as a string with $mpdf->Output(null, 'S');. If you don't need to save the PDF on your server while generating the invoice, this is the cleanest option.
Based on the variant chosen, the constructor will look like this:
public function __construct(Order $order, $data)
{
$this->data = $data; // define properties for the class
$this->fileName = 'Invoice-'.$order->id.'.pdf';
$this->order = $order;
}
and the build method will look like this:
public function build()
{
$mpdf = new \Mpdf\Mpdf();
$mpdf->WriteHTML(view('invoices.paid_invoice', [
'order' => $this->order,
'data' => $this->data
]));
$mpdf->Output($this->filename, 'F');
return $this->markdown('emails.send_order')
->subject(('my subject'))
->attach($this->filename, [
'as' => 'invoice.pdf',
'mime' => 'application/pdf',
]);
}
or this:
public function build()
{
$mpdf = new \Mpdf\Mpdf();
$mpdf->WriteHTML(/* omitted for readability */);
return $this->markdown('emails.send_order')
->subject(('my subject'))
->attachData($mpdf->Output(null, 'S'), invoice.pdf, [
'as' => 'invoice.pdf',
'mime' => 'application/pdf',
]);
}
Related
Im building project on Laravel 7.3 with multiple Jobs that run at the same time.
I need to make each Job write logs to different daily rotated file. The name of the log file should be based on model, that Job is processing.
The issue is I cant find smart solution.
What I have tried:
1) creating multiple channels in config/logging.php.
That works as expected but at the moment there are about 50 different Jobs and amount keeps growing. Method is ugly and hardly maintained.
2) setting up Config(['logging.channels.CUSTOMCHANNEL.path' => storage_path('logs/platform/'.$this->platform->name.'.log')]);.
Messing with Config variable was bad idea because of many Jobs running one time. As a result messages from one job often were written in another Job log.
3) using Log::useDailyFiles()
Seems like this stops working since laravel 5.5 or 5.6. Just getting error Call to undefined method Monolog\Logger::useDailyFiles(). Any thoughts how to make with work in laravel 7?
4) using tap parameter for channel in config/logging.php.
Example in laravel docs
No ideas how to pass model name into CustomizeFormatter to setup file name.
Im almost sure there is smart solution and Im just missing something.
Any suggests? Thanks!
You could inherit the log manager to allow a dynamic configuration
<?php
namespace App\Log;
use Illuminate\Support\Str;
use Illuminate\Log\LogManager as BaseLogManager;
class LogManager extends BaseLogManager
{
/**
* Get the log connection configuration.
*
* #param string $name
* #return array
*/
protected function configurationFor($name)
{
if (!Str::contains($name, ':')) {
return parent::configurationFor($name);
}
[$baseName, $model] = explode(':', $name, 2);
$baseConfig = parent::configurationFor($baseName);
$baseConfig['path'] = ...; //your logic
return $baseConfig;
}
}
Likewise about Laravel's log service provider except this one can be totally replaced
<?php
namespace App\Log;
use Illuminate\Support\ServiceProvider;
class LogServiceProvider extends ServiceProvider
{
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
$this->app->singleton('log', function ($app) {
return new LogManager($app);
});
}
}
EDIT: I've just seen that Laravel's log service provider is missing from config/app.php, this is because it's "hard-loaded" by the application. You still can replace it by inheriting the application itself
<?php
namespace App\Foundation;
use App\Log\LogServiceProvider;
use Illuminate\Events\EventServiceProvider;
use Illuminate\Routing\RoutingServiceProvider;
use Illuminate\Foundation\Application as BaseApplication;
class Application extends BaseApplication
{
/**
* Register all of the base service providers.
*
* #return void
*/
protected function registerBaseServiceProviders()
{
$this->register(new EventServiceProvider($this));
$this->register(new LogServiceProvider($this));
$this->register(new RoutingServiceProvider($this));
}
}
And finally in bootstrap/app.php, replace Illuminate\Foundation\Application with App\Foundation\Application
For example, if you try this
app('log')->channel('single:users')->debug('test');
Laravel will use the single channel's config and write to users.log if your resolution logic is
$baseConfig['path'] = $model + '.log';
I got a solution that I've been using since Laravel 4 that works, although it doesn't follow 'Laravel' way of doing things.
class UserTrackLogger
{
/**
* #var $full_path string
*/
protected $full_path;
/**
* #var $tenant string
*/
protected $tenant;
/**
* #var $user User
*/
protected $user;
/**
* #var $request Request
*/
protected $request;
public static function log(string $message, Request $request, User $user, array $data = []): void
{
/** #noinspection PhpVariableNamingConventionInspection */
$userTrack = new static($request, $user);
$userTrack->write($message, $data);
}
protected function __construct(Request $request, User $user)
{
$this->request = $request;
$this->user = $user;
$this->tenant = app()->make('tenant')->tenant__name;
$path = storage_path() . "/logs/{$this->tenant}/users";
$filename = $this->user->username_with_name;
$this->full_path = Formatter::formatPath("{$path}/{$filename}.log");
self::makeFolder($this->full_path);
}
protected function write(string $message, array $data = []): void
{
$formatter = $this->getFormat();
$record = [
'message' => $message,
'context' => $data,
'extra' => [],
'datetime' => date(Utility::DATETIME_FORMAT_DEFAULT),
'level_name' => 'TRACK',
'channel' => '',
];
file_put_contents($this->full_path, $formatter->format($record), FILE_APPEND);
}
protected function getFormat(): FormatterInterface
{
$ip = $this->request->getClientIp();
$method = strtoupper($this->request->method());
$format = "[%datetime%][{$this->tenant}][{$this->user->username}][{$this->user->name}]: $ip $method %message% %context%\n";
return new LineFormatter($format, null, true);
}
protected static function makeFolder(string $full_path): bool
{
$path = dirname($full_path);
if ( !is_dir($path) ) {
return mkdir($path, 0755, true);
}
return false;
}
}
And when I want to log something, I do UserTrackLogger::log($request->fullUrl(), $request, $user, $data);
What I would suggest is creating a logger similar to this but extends RotatingFileHandler.
In laravel API Resources:
I need a dynamic way to generalize a code for all resources to be used in all controllers instead of using resources in all methods for each controller .. for more clarification, I have a trait that includes generalized functions which return json responses with data and status code, lets take a "sample function" suppose it is showAll(Collection $collection) which is used for returning a collection of data of the specified model for example it is used for returning all users data ..
so I need to build a function that call what ever resource of the specified model, knowing that I have many models...
a) trait that include showAll method:
namespace App\Traits;
use Illuminate\Support\Collection;
trait ApiResponser
{
private function successResponse($data, $code) {
return response()->json($data, $code);
}
protected function showAll(Collection $collection, $code = 200) {
$collection = $this->resourceData($collection);
$collection = $this->filterData($collection);
$collection = $this->sortData($collection);
$collection = $this->paginate($collection);
$collection = $this->cacheResponse($collection);
return $this->successResponse([$collection, 'code' => $code], $code);
}
protected function resourceData(Collection $collection) {
return $collection;
}
}
b) usercontroller as a sample
namespace App\Http\Controllers\User;
use App\User;
use Illuminate\Http\Request;
use App\Http\Controllers\ApiController;
class UserController extends ApiController
{
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index()
{
$users = User::all();
// Here the showAll(Collection $collection) is used
return $this->showAll($users);
}
}
c) UserResource:
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'identity' => $this->id,
'name' => $this->name,
'email' => $this->email,
'isVerified' => $this->verified,
'isAdmin' => $this->admin,
'createDate' => $this->created_at,
'updateDate' => $this->updated_at,
'deleteDate' => $this->deleted_at,
];
}
}
generalize: means used everywhere without code redundancy
What about providers, you may load data there and make that data reachable at places where user data can be reachable ?
laravel docs
I found a simple solution.. by adding the following method
protected function resourceData($collection) {
$collection = get_class($collection[0]);
$resource = 'App\Http\Resources\\' . str_replace('App\\', '', $collection) .
'Resource';
return $resource;
}
The $collection[0] in the first line of this method will get the
model you are currently using.
get_class will get the model name ex: App\User
'App\Http\Resources\\' . str_replace('App\\', '', $collection):
This will get the path of the resource by adding 'App\Http\Resources\' before the
model
str_replace('App\\', '', $collection): will remove App\ path from the collection
name so App\User should be User
then 'Resource' would be concatenated with the previous results and the whole
string should be like that: App\Http\Resources\UserResource
So at the end you should return the whole string App\Http\Resources\UserResource
,finally you should call the resourceData() in
the showAll() method:
protected function showAll(Collection $collection, $code = 200) {
$collection = $this->resourceData($collection);
$collection = $this->filterData($collection);
$collection = $this->sortData($collection);
$collection = $this->paginate($collection);
//Calling resourceData() method
$resource = $this->resourceData($collection);
$collection = $this->cacheResponse($collection);
return $this->successResponse([$resource::collection($collection), 'code' => $code], $code);
}
I know there are dozen of questions about PHPRenderer not finding the path of a template, but I think the problem is quite different here.
First, the goal is to render a view to a variable in order to send it to a PDF Renderer (I use ZF3 TCPDF module). If there is any better way to do that, please tell me.
Here is roughly the architecture of the project: https://imgur.com/UhQ7hgP
In AlertAction() of ToolsController, I return the view like this, and it works, which make me think the template path is alright.
$view = new ViewModel();
$view->setTemplate('tools/tools/alert');
return $view;
However, when I try to render the same view with the same path in exportPDFAction(), it does not work and gives the following error.
Zend\View\Renderer\PhpRenderer::render: Unable to render template "tools/tools/alert"; resolver could not resolve to a file
The code in exportPDFAction() is:
$view = new ViewModel();
$renderer = new PhpRenderer();
$view->setTemplate('tools/tools/alert');
$html = $renderer->render($view);
I assume the last line screws it as it is the difference, but I can't get why, does anyone have any clue ?
Quite all the topics about Template path on SO were talking about the template map in module.config.php, but I think this is not the problem here since it works perfectly in AlertAction().
EDIT
The PhpRenderer is injected in the controller directly in module.config.php:
'controllers' => [
'factories' => [
ToolsController::class => function($container) {
return new ToolsController(
$container->get(Adapter::class),
$container->get(\TCPDF::class),
$container->get(PhpRenderer::class)
);
},
],
],
EDIT 2
This is the controller constructor:
public function __construct($db, $tcpdf, $renderer)
{
$this->db = $db;
$this->tcpdf = $tcpdf;
$this->renderer = $renderer;
...
}
The error you're getting might be due to the fact your Renderer is not injected via the Factory.
Try:
class MyCustomControllerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
/** #var \Zend\View\Renderer\PhpRenderer $renderer */
$renderer = $container->get('ViewRenderer')
return new MyCustomController($renderer);
}
}
In the Controller, require it be set in the __construct() function:
public function __construct(PhpRenderer $renderer)
{
// ... set it somewhere, e.g.:
$this->setRenderer($renderer);
}
Then use it in your function:
$view = new ViewModel();
$renderer = $this->getRenderer();
$view->setTemplate('tools/tools/alert');
$html = $renderer->render($view);
Why, you ask?
Because the Renderer is configured via the Zend Configuration. You can find that in the \Zend\Mvc\Service\ServiceManageFactory class. The alias configuration provided is the following:
'ViewPhpRenderer' => 'Zend\View\Renderer\PhpRenderer',
'ViewRenderer' => 'Zend\View\Renderer\PhpRenderer',
'Zend\View\Renderer\RendererInterface' => 'Zend\View\Renderer\PhpRenderer',
The alias'es are mapped to Factory:
'Zend\View\Renderer\PhpRenderer' => ViewPhpRendererFactory::class,
That Factory is:
class ViewPhpRendererFactory implements FactoryInterface
{
/**
* #param ContainerInterface $container
* #param string $name
* #param null|array $options
* #return PhpRenderer
*/
public function __invoke(ContainerInterface $container, $name, array $options = null)
{
$renderer = new PhpRenderer();
$renderer->setHelperPluginManager($container->get('ViewHelperManager'));
$renderer->setResolver($container->get('ViewResolver'));
return $renderer;
}
}
As such, it has some presets included when you use it with $this->getRenderer, namely it has the HelperPluginManager and the Resolver set. So it knows where to get additional resources (if needed) and it knows how to resolve (ie render) a View.
Alright so I'm pretty new to both unit testing, mockery and laravel. I'm trying to unit test my resource controller, but I'm stuck at the update function. Not sure if I'm doing something wrong or just thinking wrong.
Here's my controller:
class BooksController extends \BaseController {
// Change template.
protected $books;
public function __construct(Book $books)
{
$this->books = $books;
}
/**
* Store a newly created book in storage.
*
* #return Response
*/
public function store()
{
$data = Input::except(array('_token'));
$validator = Validator::make($data, Book::$rules);
if($validator->fails())
{
return Redirect::route('books.create')
->withErrors($validator->errors())
->withInput();
}
$this->books->create($data);
return Redirect::route('books.index');
}
/**
* Update the specified book in storage.
*
* #param int $id
* #return Response
*/
public function update($id)
{
$book = $this->books->findOrFail($id);
$data = Input::except(array('_token', '_method'));
$validator = Validator::make($data, Book::$rules);
if($validator->fails())
{
// Change template.
return Redirect::route('books.edit', $id)->withErrors($validator->errors())->withInput();
}
$book->update($data);
return Redirect::route('books.show', $id);
}
}
And here are my tests:
public function testStore()
{
// Add title to Input to pass validation.
Input::replace(array('title' => 'asd', 'content' => ''));
// Use the mock object to avoid database hitting.
$this->mock
->shouldReceive('create')
->once()
->andReturn('truthy');
// Pass along input to the store function.
$this->action('POST', 'books.store', null, Input::all());
$this->assertRedirectedTo('books');
}
public function testUpdate()
{
Input::replace(array('title' => 'Test', 'content' => 'new content'));
$this->mock->shouldReceive('findOrFail')->once()->andReturn(new Book());
$this->mock->shouldReceive('update')->once()->andReturn('truthy');
$this->action('PUT', 'books.update', 1, Input::all());
$this->assertRedirectedTo('books/1');
}
The issue is, when I do it like this, I get Mockery\Exception\InvalidCountException: Method update() from Mockery_0_Book should be called exactly 1 times but called 0 times. because of the $book->update($data) in my controller. If I were to change it to $this->books->update($data), it would be mocked properly and the database wouldn't be touched, but it would update all my records when using the function from frontend.
I guess I simply just want to know how to mock the $book-object properly.
Am I clear enough? Let me know otherwise. Thanks!
Try mocking out the findOrFail method not to return a new Book, but to return a mock object instead that has an update method on it.
$mockBook = Mockery::mock('Book[update]');
$mockBook->shouldReceive('update')->once();
$this->mock->shouldReceive('findOrFail')->once()->andReturn($mockBook);
If your database is a managed dependency and you use mock in your test it causes brittle tests.
Don't mock manage dependencies.
Manage dependencies: dependencies that you have full control over.
In ZF1 I used to declare variables in the application.ini
brandname = "Example"
weburl = "http://www.example.com/"
assetsurl = "http://assets.example.com/"
And in the Bootstrap I did this so i could access them in the view
define('BRANDNAME', $this->getApplication()->getOption("brandname"));
define('WEBURL', $this->getApplication()->getOption("weburl"));
define('ASSETSURL', $this->getApplication()->getOption("assetsurl"));
Whats the ZF2 way to do this, I know that i can create an array in the local.php config file like:
return array(
'example' => array(
'brandname' => 'Example',
'weburl' => 'http://www.example.com/',
'asseturl' => 'http://assets.example.com/',
),
);
When I want to access that variable in the controller I can do
$config = $this->getServiceLocator()->get('Config');
$config['example']['brandname']);
So far so good... but how do i access this variable in the view?
I don't want to create a view variable for it in every controller. And when i try the above in a view phtml file i get an error.
Zend\View\HelperPluginManager::get was unable to fetch or create an instance for getServiceLocator
Any ideas?
You could create a sinmple view helper to act as a proxy for your config, (totally un tested).
Module.php
public function getViewHelperConfig()
{
return array(
'factories' => array(
'configItem' => function ($helperPluginManager) {
$serviceLocator = $helperPluginManager->getServiceLocator();
$viewHelper = new View\Helper\ConfigItem();
$viewHelper->setServiceLocator($serviceLocator);
return $viewHelper;
}
),
);
}
ConfigItem.php
<?php
namespace Application\View\Helper;
use Zend\View\Helper\AbstractHelper;
use Zend\ServiceManager\ServiceManager;
/**
* Returns total value (with tax)
*
*/
class ConfigItem extends AbstractHelper
{
/**
* Service Locator
* #var ServiceManager
*/
protected $serviceLocator;
/**
* __invoke
*
* #access public
* #param string
* #return String
*/
public function __invoke($value)
{
$config = $this->serviceLocator->get('config');
if(isset($config[$value])) {
return $config[$value];
}
return NULL;
// we could return a default value, or throw exception etc here
}
/**
* Setter for $serviceLocator
* #param ServiceManager $serviceLocator
*/
public function setServiceLocator(ServiceManager $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
}
}
You could then do something like this in your view, assuming you have them set in your config of course :)
echo $this->configItem('config_key');
echo $this->configItem('web_url');
I would personally tend to just pass the values through to the view every time though, keeping the view a dumb as possible.
I answered this before on a different post.
/* Inside your action controller method */
// Passing Var Data to Your Layout
$this->layout()->setVariable('stack', 'overflow');
// Passing Var Data to Your Template
$viewModel = new ViewModel(array( 'stack' => 'overflow' ));
/* In Either layout.phtml or {Your Template File}.phtml */
echo $this->stack; // Will print overview
That's it... No need to mess with view helpers, event manager, service manager, or anything else.
Enjoy!