I am trying to use this package to push notifications to users via OneSignal. However I needed to make a little change. My API serves two (related) apps and I have two OneSignal configs. I am trying to override its ServiceProvider (using this technique).
The ServiceProvider presents itself as follows
<?php
namespace NotificationChannels\OneSignal;
use Berkayk\OneSignal\OneSignalClient;
use Illuminate\Support\ServiceProvider;
use NotificationChannels\OneSignal\Exceptions\InvalidConfiguration;
class OneSignalServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*/
public function boot()
{
$this->app->when(OneSignalChannel::class)
->needs(OneSignalClient::class)
->give(function () {
$oneSignalConfig = config('services.onesignal');
if (is_null($oneSignalConfig)) {
throw InvalidConfiguration::configurationNotSet();
}
return new OneSignalClient(
$oneSignalConfig['app_id'],
$oneSignalConfig['rest_api_key'],
''
);
});
}
}
The behavior that I want to change is located in the line
$oneSignalConfig = config('services.onesignal');
As it assumes that my config/services.php has the following entry (stated in the doc) :
// config/services.php
...
'onesignal' => [
'app_id' => env('ONESIGNAL_APP_ID'),
'rest_api_key' => env('ONESIGNAL_REST_API_KEY')
],
...
Whereas I want to set my config/services.php as follows
// config/services.php
...
'onesignal' => [
'app1' => [
'app_id' => env('ONESIGNAL_1_APP_ID'),
'rest_api_key' => env('ONESIGNAL_1_REST_API_KEY')
],
'app2' => [
'app_id' => env('ONESIGNAL_2_APP_ID'),
'rest_api_key' => env('ONESIGNAL_2_REST_API_KEY')
],
],
...
And I want somehow to tell my ServiceProvider (through some kind of parameter) to either do
$oneSignalConfig = config('services.onesignal.app1');
OR
$oneSignalConfig = config('services.onesignal.app2');
But I didn't find any way to pass a parameter to the class, the boot function or the give method (and if I understood well I shouldn't even be doing that).
The only way I could think of is to create two classes that extend the OneSignalChannel::class
and duplicate code in the boot function so it becomes as follows :
public function boot()
{
$this->app->when(FirstOneSignalChannel::class)
->needs(OneSignalClient::class)
->give(function () {
$oneSignalConfig = config('services.onesignal.app1');
if (is_null($oneSignalConfig)) {
throw InvalidConfiguration::configurationNotSet();
}
return new OneSignalClient(
$oneSignalConfig['app_id'],
$oneSignalConfig['rest_api_key'],
''
);
});
$this->app->when(SecondOneSignalChannel::class)
->needs(OneSignalClient::class)
->give(function () {
$oneSignalConfig = config('services.onesignal.app2');
if (is_null($oneSignalConfig)) {
throw InvalidConfiguration::configurationNotSet();
}
return new OneSignalClient(
$oneSignalConfig['app_id'],
$oneSignalConfig['rest_api_key'],
''
);
});
}
The difference in the when provoking a difference in the config but it seems a lot of duplication and not extensible (what if I had three apps).
Should I use this method, or is there a way to pass a parameter to this ServiceProvider or is there another solution ?
https://stackoverflow.com/a/34224082/10371024
I could see what you need, but to pass parameter to boot method is not a good idea according to Laravel architecture. You may try to get what you want with using events as Vladislav suggested.
Related
I registered LogConnectionFailed like this:
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
'Illuminate\Http\Client\Events\ConnectionFailed' => [
'App\Listeners\LogConnectionFailed',
],
];
The ConnectionFailed event is fired if no response is received for a given request.
my class {
public function send() {
$response = Http::get('http://example.com');
}
}
I need to The name of the class and the method in which this happened and duration time call http client in LogConnectionFailed class.
This is not possible through normal parameter passing, so I utilized PHP native function debug_backtrace() and hacked through it.
The logic is that when the listener wants to handle the event, we get the callback trace and filter through the call stack frames until we find one of our watching location.
The code is this:
use Illuminate\Support\Str;
use Illuminate\Http\Client\Events\ConnectionFailed;
class LogConnectionFailed
{
/**
* Create the event listener.
*
* #return void
*/
public function __construct()
{
//
}
public function handle(ConnectionFailed $event)
{
$backtraceCollection = Collect(debug_backtrace());
$callerClass = $backtraceCollection->first(function($value, $key) use ($event){
$class = $value['class'] ?? '';
return $this->classIsWatched($class);
});
if ($callerClass) {
// Store in DB or do some other stuff.
dd([
'class' => $callerClass['class'],
'function' => $callerClass['function'],
'line' => $callerClass['line'],
]);
} else {
dd("should skip. Not Watching classes.");
}
}
private function classIsWatched(string $className): bool
{
return Str::is(
['App\Http\Controllers\*', 'App\MyClass'],
$className
);
}
}
Here take note at the array inside the function classIsWatched:
['App\Http\Controllers\*', 'App\MyClass']
These are the classes or directories we will watch, which means if the ConnectionFailed due to some calls from these classes, they will be captured, else they will be skipped. This gives you the flexibility to just filter out and watch certain locations inside your application.
Note that we can also use wildcards * for simplifying the path inclusions. For example App\Http\Controllers\Api\EventController is watched too.
For example if I have this class inside the App path:
namespace App;
use Illuminate\Support\Facades\Http;
class MyClass
{
public static function callEvent()
{
$response = Http::get('http://example.com');
}
}
due to any reason if a ConnectionFailed event dispatches, the output of handle method will be:
array:3 [▼
"class" => "App\MyClass"
"function" => "callEvent"
"line" => 11
]
this will give you the class name, function name and even the line which event was raised there. You can simply replace the dd() inside the handle method of the listener and do what you want to do with the data.
About the Http Call duration, no accurate solution comes to my mind, but you can have a rough estimation using this approach:
dd(microtime(true) - LARAVEL_START);
add the above code inside the handle method too, and this gives you the time difference from the moment that the application started and till you got this point (Http request failed and you got inside this listener).
I have recently been learning about the AppServiceProvider. I have registered a service in the AppServiceProvider which creates a singleton - an instantiated GuzzleHttp Client, like so:
$this->app->singleton('GuzzleHttp\Client', function($api) {
return new Client([
'base_uri' => env('ELASTICSEARCH_HOST'),
'auth' => [
env('ELASTICSEARCH_USER'),
env('ELASTICSEARCH_PASS')
],
]);
});
This is connecting to an ElasticSearch API, and that currently works:
$response = app('GuzzleHttp\Client')->request('GET');
I have set up a facade called ElasticSearchFacade, which contains only the getFacadeAccessor():
protected static function getFacadeAccessor()
{
return 'elasticSearch';
}
I have also registered elasticSearch in my AppServiceProvider, like so:
$this->app->bind('elasticSearch', function() {
return new ElasticSearch();
});
This creates a new ElasticSearch instance. However, I would love to pass the GuzzleHttp\Client into the elasticSearch service. So I have tried adding the following to my ElasticSearch.php file:
use GuzzleHttp\Client;
class ElasticSearch
{
protected $client;
public function __contruct(Client $client)
{
$this->client = $client;
}
public function handle()
{
$response = $this->client->request('GET');
die($response->getBody()->getContents());
}
}
I have now changed the registered service to pass through the GuzzleHttp Client like so:
$this->app->bind('elasticSearch', function() {
return new ElasticSearch(app('GuzzleHttp\Client'));
});
However I am getting the error:
PHP Error: Call to a member function request() on null
The constructor method is __construct not __contruct. You have not defined a custom constructor for your ElasticSearch class. So that member variable is null.
Side Note: do not call env outside of the configuration files.
To avoid having to make these env calls outside of configuration files you can just add configuration files as needed or add to current configuration files. Something like Elastic Search credentials can probably get added to the services.php configuration file:
<?php
return [
...
'elasticsearch' => [
'host' => env('ELASTICSEARCH_HOST'),
'user' => env('ELASTICSEARCH_USER'),
'password' => env('ELASTICSEARCH_PASS'),
],
...
];
Now that you have these in the configuration you can use the configuration system to pull these values:
config('services.elasticsearch'); // that whole array of values
config('services.elasticsearch.host'); // just that host value
Config::get('services.elasticsearch');
app('config')->get(...);
There are multiple ways to access the configuration system.
i am working on facebook messenger bot. I am using Botman (botman.io) without Laravel or botman studio. Version of PHP is 7.4.
Simple hears and reply method works fine, but conversation replying method does not working.
If I try type hi|hello or some greetings, chatbot answer me "Hello! What is your firstname?", then I write my name and chatbot does not returns any text :-/
Can you help me where is a bug?
There is a conversation class:
namespace LiborMatejka\Conversations;
use BotMan\BotMan\Messages\Conversations\Conversation;
use BotMan\BotMan\Messages\Incoming\Answer;
class OnboardingConversation extends Conversation {
protected $firstname;
protected $email;
function askFirstname() {
$this->ask('Hello! What is your firstname?', function (Answer $answer) {
// Save result
$this->firstname = $answer->getText();
$this->say('Nice to meet you ' . $this->firstname);
$this->askEmail();
});
}
public function askEmail() {
$this->ask('One more thing - what is your email?', function (Answer $answer) {
// Save result
$this->email = $answer->getText();
$this->say('Great - that is all we need, ' . $this->firstname);
});
//$this->bot->typesAndWaits(2);
}
public function run() {
// This will be called immediately
$this->askFirstname();
}
}
and there is config:
require_once "vendor/autoload.php";
require_once "class/onboardingConversation.php";
use BotMan\BotMan\BotMan;
use BotMan\BotMan\BotManFactory;
use BotMan\BotMan\Drivers\DriverManager;
use BotMan\Drivers\Facebook\FacebookDriver;
use LiborMatejka\Conversations\OnboardingConversation;
$config = [
// Your driver-specific configuration
'facebook' => [
'token' => 'my_token',
'app_secret' => 'my_secret_app_code',
'verification' => 'verification_code',
],
'botman' => [
'conversation_cache_time' => 0,
],
];
// Load the driver(s) you want to use
DriverManager::loadDriver(\BotMan\Drivers\Facebook\FacebookDriver::class);
// Create an instance
$botman = BotManFactory::create($config);
$botman->hears('ahoj|hi|hello|cau|cus|zdar|zdarec|cago|hey|ciao', function (BotMan $bot) {
$bot->startConversation(new OnboardingConversation);
});
// Start listening
$botman->listen();
Add symfony/cache to your project using composer
composer require symfony/cache
Put following at top of index.php (or other) file where you're setting up BotMan
use BotMan\BotMan\Cache\SymfonyCache;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
Then create your BotMan using the following:
$adapter = new FilesystemAdapter();
$botman = BotManFactory::create($config, new SymfonyCache($adapter));
Then use your $botman variable accordingly, like example below:
$botman->hears('Hi', function (BotMan $bot) {
$bot->typesAndWaits(2);
$bot->reply('Hello and welcome');
$bot->typesAndWaits(2);
$bot->ask('Anything I can do for you today?',function($answer, $bot){
$bot->say("Oh, really! You said '{$answer->getText()}'... is that right?");
});
});
I would rather to use auto-wiring to inject the SymfonyCache anywhere you create the Botman instance, without creating the adapter and cache again and again.
Step 1: config the cache in cache.yaml
framework:
cache:
# botman: cache adapter
app: cache.adapter.filesystem
Step 2: autowiring in services.yaml
services:
BotMan\BotMan\Cache\SymfonyCache:
arguments:
$adapter: '#cache.app'
Step 3: Inject the SymfonyCache where you need, for example in ChatController::message()
public function message(SymfonyCache $symfonyCache): Response
{
....
$botman = BotManFactory::create([], $symfonyCache);
....
$botman->hears(
'survey',
function (BotMan $bot) {
$bot->startConversation(new OnBoardingConversation());
}
);
}
To create the OnBoardingConversation, just follow the documentation on create a conversation in botman
I am currently working on API in laravel 5.6 and I would like versioning for APIs like v1 and v2.
my problem is I want to run one URL and access both API version, I am passing version number into HEADER and access API controller according to the version number. I am also using middle ware to check version number but not getting what I need.
Here is my web.php
Route::post('/api/getticktes/{id}', 'Api\v1\TicketController#show')->middleware('checkHeaderV1');
Header
version :- v1
version :- v2
My controller directory is
Controller
-Api
--v1
---TicketController.php
--v2
---TicketController.php
You can use this class, i have used it in my old project, may be it is not the efficient way but it will work
ApiVersion class
namespace App\Http;
use Illuminate\Http\Request;
class ApiVersion
{
protected static $valid_api_versions = [
1 => 'v1',
2 => 'v2'
];
protected static function get($request)
{
$allApiVersions = array_keys(self::$valid_api_versions);
$latestVersion = $allApiVersions[count($allApiVersions) - 1];
$apiVersion = $request->header('api-version', $latestVersion);
return in_array($apiVersion, $allApiVersions) ? $apiVersion : $latestVersion;
}
protected static function getNamespace($apiVersion)
{
return 'Api\\' . self::$valid_api_versions[$apiVersion];
}
public static function versionNamespace()
{
$request = Request::capture();
return $apiNamespace = ApiVersion::getNamespace(self::get($request));
}
}
Now use this middleware in api routes file for namespace
//API routes
$versionNameSpace = ApiVersion::versionNamespace();
Route::group(['middleware' => ['api'], 'namespace' => "{$versionNameSpace}"], function () {
});
Add api-version in your request header. The value should be 1 or 2 etc
I'm using Lumen (Laravel) to create an API for an online activity/campaign application which handles things like on-site registrations and gift redemptions for various events. Occasionally, there are certain events with very specific functionality required which need their own custom logic. I'm wondering how to best handle this custom code from an architecture/best practices standpoint.
Here's what I have: I have a route which calls a CustomCampaignController like so:
$router->group([
'prefix' => 'v1'
], function () use ($router) {
// ..... other routes for standard activities
$router->post('customCampaigns', 'CustomCampaignController#runController');
});
Under App\Http\Controllers I've opened a directory to store classes for all custom activities. The customCampaigns route takes an activityId parameter whose value matches one of the activity classes. For example if the client posts activityId="MyCustomActivity" to customCampaigns, I would instantiate the following class: App\Http\Controllers\Custom\MyExampleActivity.
// app/Http/Controllers/CampaignController.php
public function runController(Request $request) {
$className = 'App\\Http\\Controllers\\Custom\\' . $request->input('activityId');
$customController = new $className;
return $customController->run();
}
The custom controller would then do its thing and return the response
// app/Http/Controllers/Custom/MyCustomActivity.php
namespace App\Http\Controllers\Custom;
class MyCustomActivity
{
public function __construct()
{
//
}
public function run()
{
// Custom logic here
return response('Response');
}
}
Is this a good approach or could it be considered an anti-pattern? Please let me know if there is another pattern for this type of problem.
I would prefer put the custom activity as the part of url. So, you will have something like this
$router->group([
'namespace' => 'App\Http\Controllers\Custom',
'prefix' => 'v1/customCampaigns'
], function () use ($router) {
$router->post('myCustomActivity', 'MyCustomActivityController#methodName');
});
Using this format, you can map the endpoint directly into a specific controller.