How to bind configuration value Laravel service provider on run-time? - php

I have created custom service provider which extends XeroServiceProvide, Basically, I have multiple Xero Account and I want to change two configuration params value runtime consumer_key and consumer_secret. Is there a quick way. I have checked Service Container contextual binding but don't know how to use.
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use DrawMyAttention\XeroLaravel\Providers\XeroServiceProvider;
class CustomXeroServiceProvider extends XeroServiceProvider
{
private $config = 'xero/config.php';
/**
* Bootstrap the application services.
*
* #return void
*/
public function boot()
{
//
parent::boot();
}
/**
* Register the application services.
*
* #return void
*/
public function register($configParams = [])
{
parent::register();
if(file_exists(config_path($this->config))) {
$configPath = config_path($this->config);
$config = include $configPath;
}
$this->app->bind('XeroPrivate', function () use ($config,$configParams) {
if(is_array($configParams) && count($configParams) > 0){
if(isset($configParams['consumer_key'])){
$config['oauth']['consumer_key'] = $configParams['consumer_key'];
}
if(isset($configParams['consumer_secret'])){
$config['oauth']['consumer_secret'] = $configParams['consumer_secret'];
}
}
return new \XeroPHP\Application\PrivateApplication($config);
});
}
}
From Controller I tried changing value like this but bind params not changing dynamically
foreach($centers as $center) {
config(['xero.config.oauth.consumer_key' => $center->consumer_key]);
config(['xero.config.oauth.consumer_secret' => $center->consumer_secret]);
}
Update 2
Is there a way I can rebind service container after updating config file values or somehow i can refresh service provider binding?
config(['xero.config.oauth.consumer_key' => 'XXXXXXX']);
config(['xero.config.oauth.consumer_secret' => 'XXXXXX']);
// rebind Service provider after update

This is how I ended up doing. I have created a custom function that sets value on runtime.
/**
* connect to XERO by center
* #param $center
* #return mixed
*/
public static function bindXeroPrivateApplication($center){
$configPath = config_path('xero/config.php');
$config = include $configPath;
config(['xero.config.oauth.consumer_key' => $center->consumer_key]);
config(['xero.config.oauth.consumer_secret' => $center->consumer_secret]);
return \App::bind('XeroPrivate', function () use ($config,$center) {
$config['oauth']['consumer_key'] = $center->consumer_key;
$config['oauth']['consumer_secret'] = $center->consumer_secret;
return new \XeroPHP\Application\PrivateApplication($config);
});
}
I have model called Center.php and I am calling above function from that same model as below.
$center = Center::find(1);
self::bindXeroPrivateApplication($center);

Related

Add custom function to Laravel query builder

I am trying to add USE INDEX() to the query builder in Laravel. I tried to follow similar steps to link and was kind of successful but I cannot manage the last bit and I am not sure my ad-hoc code has created a huge backdoor.
The target: The target of my exercise is to add Index to the query builder like below:
DB::table('users')->where('id',1)->**useIndex**('users')->get()->first();
Here an option useIndex specifies the index that I am going to use for this query.
What I have done yet: Created a class named Connection in App/Override
<?php
namespace App\Override;
class Connection extends \Illuminate\Database\MySqlConnection {
//#Override
public function query() {
return new QueryBuilder(
$this,
$this->getQueryGrammar(),
$this->getPostProcessor()
);
}
}
Created a service provider named CustomDatabaseServiceProvider in App/Providers. Here I just manipulated registerConnectionServices function. I further commented Illuminate\Database\DatabaseServiceProvider::class, and added App\Providers\CustomDatabaseServiceProvider::class, to app.php in config directory.
<?php
namespace App\Providers;
use App\Override\Connection;
use Illuminate\Database\DatabaseManager;
use Illuminate\Database\Query\Grammars\Grammar;
use Illuminate\Database\Schema;
use Illuminate\Contracts\Queue\EntityResolver;
use Illuminate\Database\Connectors\ConnectionFactory;
use Illuminate\Database\Eloquent\Factory as EloquentFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\QueueEntityResolver;
use Illuminate\Support\ServiceProvider;
class CustomDatabaseServiceProvider extends ServiceProvider
{
/**
* The array of resolved Faker instances.
*
* #var array
*/
protected static $fakers = [];
/**
* Bootstrap the application events.
*
* #return void
*/
public function boot()
{
Model::setConnectionResolver($this->app['db']);
Model::setEventDispatcher($this->app['events']);
}
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
Model::clearBootedModels();
$this->registerConnectionServices();
$this->registerEloquentFactory();
$this->registerQueueableEntityResolver();
}
/**
* Register the primary database bindings.
*
* #return void
*/
protected function registerConnectionServices()
{
// The connection factory is used to create the actual connection instances on
// the database. We will inject the factory into the manager so that it may
// make the connections while they are actually needed and not of before.
$this->app->singleton('db.factory', function ($app) {
return new ConnectionFactory($app);
});
// The database manager is used to resolve various connections, since multiple
// connections might be managed. It also implements the connection resolver
// interface which may be used by other components requiring connections.
$this->app->singleton('db', function ($app) {
$dbm = new DatabaseManager($app, $app['db.factory']);
//Extend to include the custom connection (MySql in this example)
$dbm->extend('mysql', function ($config, $name) use ($app) {
//Create default connection from factory
$connection = $app['db.factory']->make($config, $name);
//Instantiate our connection with the default connection data
$new_connection = new Connection(
$connection->getPdo(),
$connection->getDatabaseName(),
$connection->getTablePrefix(),
$config
);
//Set the appropriate grammar object
// $new_connection->setQueryGrammar(new Grammar());
// $new_connection->setSchemaGrammar(new Schema\());
return $new_connection;
});
return $dbm;
});
$this->app->bind('db.connection', function ($app) {
return $app['db']->connection();
});
}
/**
* Register the Eloquent factory instance in the container.
*
* #return void
*/
protected function registerEloquentFactory()
{
$this->app->singleton(FakerGenerator::class, function ($app, $parameters) {
$locale = $parameters['locale'] ?? $app['config']->get('app.faker_locale', 'en_US');
if (!isset(static::$fakers[$locale])) {
static::$fakers[$locale] = FakerFactory::create($locale);
}
static::$fakers[$locale]->unique(true);
return static::$fakers[$locale];
});
$this->app->singleton(EloquentFactory::class, function ($app) {
return EloquentFactory::construct(
$app->make(FakerGenerator::class), $this->app->databasePath('factories')
);
});
}
/**
* Register the queueable entity resolver implementation.
*
* #return void
*/
protected function registerQueueableEntityResolver()
{
$this->app->singleton(EntityResolver::class, function () {
return new QueueEntityResolver;
});
}
}
and finally created a class named QueryBuilder in App/Override. this is the problematic class:
<?php
namespace App\Override;
use Illuminate\Support\Facades\Cache;
class QueryBuilder extends \Illuminate\Database\Query\Builder
{
private $Index = [];
public function useIndex($index = null)
{
$this->Index = $index;
return $this;
}
//#Override
public function get($columns = ['*'])
{
if ($this->Index) {
//Get the raw query string with the PDO bindings
$sql_str = str_replace('from `' . $this->from . '`', 'from `' . $this->from . '` USE INDEX (`' . $this->Index . '`) ', $this->toSql());
$sql_str = vsprintf($sql_str, $this->getBindings());
return parent::get($sql_str);
} else {
//Return default
return parent::get($columns);
}
}
}
The issues here are:
The output does not contain USE INDEX
Is it safe to use str_replace to manipulate query?
The query builder is macroable so in your service provider you can probably do:
Illuminate\Database\Query\Builder::macro(
'tableWithIndex',
function ($table, $index) {
$table = $this->grammar->wrapTable($table);
$index = $this->grammar->wrap($index);
return $this->fromRaw("$table USE INDEX ($index)");
}
);
Then you could use this:
DB::tableWithIndex('users', 'users');
within the macro $this would refer to the query builder instance
Note that I have them both in one because you can potentially have multiple from calls for the same query and it would be a mess trying to figure out what goes where

Laravel 7 set log path dynamically in Job class

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.

Laravel Event listener and caching not working

I am facing some difficulties while developing an app on Laravel.
I want to use Event and Listener to delete and rebuild the cache of an object.
Here is the code:
app\Events\CampaignEvent.php
namespace App\Events;
use Illuminate\Queue\SerializesModels;
class CampaignEvent extends Event
{
use SerializesModels;
public $user_id;
public $cache_keys;
/**
* Create a new event instance.
*
* #return void
*/
public function __construct($user_id, $cache_keys)
{
$this->user_id = $user_id;
$this->cache_keys = $cache_keys;
}
}
app\Listenters\CampaignListener.php
<?php
namespace App\Listeners;
use App\Events\CampaignEvent;
use Cache;
use Log;
use App\BrandCampaign;
class CampaignListener
{
/**
* Handle the event.
*
* #param CampaignEvent $event
* #return void
*/
public function handle(CampaignEvent $event)
{
/**
* Remove cache
*/
if(is_array($event->cache_keys)){
foreach($event->cache_keys as $index => $cache_key){
\Cache::forget($cache_key);
Log::debug("[CACHE] Deleted cache for: " . $cache_key);
}
} else {
\Cache::forget($event->cache_keys);
Log::debug("[CACHE] Deleted cache for: " . $event->cache_keys);
}
/**
* Rebuild cache for BrandCampaigns
*/
$campaigns = BrandCampaign::with(['influencers' => function($query){
$query->with(['influencer' => function($query){
$query->select('id','profile_picture');
}])->latest();
}])->where('user_id', $event->user_id )->latest()->get();
$total_influencers = [];
foreach($campaigns as $campaign){
foreach ($campaign->influencers as $influencer) {
if(!in_array($influencer->influencer_id, $total_influencers))
$total_influencers[] = $influencer->influencer_id;
}
}
$total_influencers = count($total_influencers);
$campaigns = collect($campaigns)->toArray();
\Cache::forever('#suppliers_campaigns('.$event->user_id.')', $campaigns);
\Cache::put('#suppliers_total_campaigns('.$event->user_id.')', $total_influencers, 10);
Log::debug("[CACHE] Cache rebuilt successfully!");
return $event;
}
}
I want to cache an array "forever", but in my campaign controller, after the event is fired, when I pull the array from cache it is returning null
Thanks!
Works in Laravel 5 (based on the question) & Laravel 7 (latest) as well.
use Illuminate\Support\Facades\Cache;
// Remove cache
Cache::forget('brandCampaigns');
// Rebuild cache for BrandCampaigns. Here, when the cache key doesn't exists, the function will be called and the returned value will be stored in the cache
$campaigns = Cache::rememberForever('brandCampaigns', function () {
return BrandCampaign::with(['influencers' => function ($query) {
$query->with(['influencer' => function ($query) {
$query->select('id', 'profile_picture');
}])->latest();
}])->where('user_id', $event->user_id)->latest()->get();
});
It is important to enable discovery in EventServiceProvider class.
-> app/Providers/EventServiceProvider.php
public function shouldDiscoverEvents()
{
return true;
}
make sure this function return true, otherwise events and listeners don't find together.

Laravel ioc automatic resolution - works from controller but not from custom class

Namespaces omitted for brevity...
I have written the following service provider and registered in config/app.php:
class OfferServiceProvider extends ServiceProvider
{
public function register()
{
$this->registerLossControlManager();
}
protected function registerLossControlManager()
{
$this->app->bind('LossControlInterface', 'LossControl');
}
}
Here is my LossControlInterface
interface LossControlInterface
{
/**
* #param int $demandId
* #param float $offerTotal
* #param float $productTotal
* #param null|int $partnerId
* #return mixed
*/
public function make($demandId, $offerTotal, $productTotal, $partnerId = null);
/**
* #return float
*/
public function getAcceptableLoss();
/**
* #return bool
*/
public function isAcceptable();
/**
* #return bool
*/
public function isUnacceptable();
/**
* #return null
*/
public function reject();
}
Now within the controller, I can inject the LossController as follows:
use LossControlInterface as LossControl;
class HomeController extends BaseController {
public function __construct(LossControl $lossControl)
{
$this->lossControl = $lossControl;
}
public function getLossThresholds()
{
$lossControl = $this->lossControl->make(985, 1000, null);
var_dump('Acceptable Loss: ' . $lossControl->getAcceptableLoss());
var_dump('Actual Loss: ' . $lossControl->calculateLoss());
var_dump('Acceptable? ' . $lossControl->isAcceptable());
}
}
However if I try to dependency inject the LossControlInterface from within a custom class called by a command:
[2014-09-02 13:09:52] development.ERROR: exception 'ErrorException' with message 'Argument 11 passed to Offer::__construct() must be an instance of LossControlInterface, none given, called in /home/vagrant/Code/.../ProcessOffer.php on line 44 and defined' in /home/vagrant/Code/.../Offer.php:79
It appears as though I am unable to dependency inject the interface into a custom class, but I can when dependency injecting into a controller.
Any thoughts on what Im doing wrong or have omitted to get the automatic resolution working?
The IoC is automatic within controllers, and you don't see the injection because Laravel handles the construction of controllers for you. When creating any other custom class by using the new keyword, you will still need to send in all of the parameters needed to it's constructor:
$myClass = new ClassWithDependency( app()->make('Dependency') );
You can hide this, to a degree, by funneling creation of your custom class through a service provider:
// Your service provider
public function register()
{
$this->app->bind('ClassWithDependency', function($app) {
return new ClassWithDependency( $app->make('Dependency') );
});
}
Then just have the IoC make it whenever you need it:
$myClass = app()->make('ClassWithDepenency');
In your case, you can change your code to look like this:
private function setOffer(Offer $offer = null) {
$this->processOffer = $offer ?:
new Offer( app()->make('LossControlInterface') );
}
A perhaps cleaner approach could be to create a service provider and an OfferFactory which gets injected into your controller. The controller can then request the factory to create the offer whenever it needs one:
// Controller
public function __construct(OfferFactory $offerFactory)
{
$this->offerFactory = $offerFactory;
}
public function setOffer(Offer $offer = null)
{
$this->processOffer = $offer ?: $this->offerFactory->createOffer();
}
// OfferFactory
class OfferFactory
{
public function createOffer()
{
return app()->make('Offer');
}
}
This has the benefit of completely decoupling your controller from the logic behind the creation of the offer, yet allowing you to have a spot to add any amount of complexity necessary to the process of creating offers.
In Laravel 5.2 the simplest solution for your particular problem would be to replace
new Offer();
with
App::make('Offer');
or even shorter
app('Offer');
which will use Laravel Container to take care of dependencies.
If however you want to pass additional parameters to the Offer constructor it is necessary to bind it in your service provider
App::bind('Offer', function($app, $args) {
return new Offer($app->make('LossControl'), $args);
});
And voila, now you can write
app('Offer', [123, 456]);
In laravel 5.4 (https://github.com/laravel/framework/pull/18271) you need to use the new makeWith method of the IoC container.
App::makeWith( 'App\MyNameSpace\MyClass', [ $id ] );
if you still use 5.3 or below, the above answers will work.

ZF2 How to use global variables in the view

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!

Categories