Pulling my hair out over some weird behaviour.
Essentially I've got a Class that constructs a GuzzleHttp\Client and a custom object of organization data, like so:
// Config set-up
$config = [
'ex1' => [
'api_key' => getenv('EX1_API_KEY'),
'org_id' => getenv('EX1_ORG_ID'),
],
'ex2' => [
'api_key' => getenv('EX2_API_KEY'),
'org_id' => getenv('EX2_ORG_ID'),
],
'ex3' => [
'api_key' => getenv('EX3_API_KEY'),
'org_id' => getenv('EX3_ORG_ID'),
],
];
// Initialize adapters
$ex1 = new Adapter($config['ex1']);
$ex2 = new Adapter($config['ex2']);
$ex3 = new Adapter($config['ex3']);
Which is all a-okay, until they finish their construction with $this->org = $org, which overwrites all of them with the same $org, in this line inside the constructor:
// Construct connected org
$org = Organization::get($this, $args['org_id']);
$this->org = $org;
The frustrating part in all of this is if I assign a property of that org instead of the whole thing, each item comes through unique (e.g. $this->org = $org->name).
I have a feeling this has to do with my Organization class, but I don't know where to start debugging this. Can provide more code/context on request, but the entire code-base is on GitHub.
I structured my abstract Resource class as a Singleton pattern. Because of this, I could not have more than one instance at a time (thus, Singleton) and as such was just changing the properties of the same instance each time.
Related
I'm using the Log:: facade a lot and have a helper class called LogHelper which provide me with a static method LogHelper::context() which include many key values I need to track the requests. But having to type it every time for each usage make it error prune and fill not so efficient.
I'm looking for a way to inject the values by default, and allow me to overwrite them if needed specifically.
At the moment this is how I use it,
Log::debug('Request Started', LogHelper::context());
what I'm looking for is to inject the context by default
Log::debug('Request Started');
and have the option to overwrite it, if need it:
Log::debug('Request Started', ['more' => 'context'] + LogHelper::context());
PS, the LogHelper::context() return a simple key => value array which include some staff i need to debug requests, and the reason it do not use the values directly in the message is because i log to graylog as structured data, and this way i can filter by any key.
I have solved this issue by using the tap functionality and $logger->withContext() (note: the latter was added in Laravel 8.49).
You want to create a new class which contains your context logic. I've created an extra Logging folder in app/ in which my logging customizations sit.
app/Logging/WithAuthContext.php:
<?php
namespace App\Logging;
use Illuminate\Log\Logger;
class WithAuthContext
{
public function __invoke(Logger $logger)
{
$logger->withContext([
'ip' => request()?->ip(),
'ua' => request()?->userAgent(),
]);
}
}
Depending on which logging channel(s) you use, you will have to add the class to each one you want to add context to. So in app/config/logging.php:
<?php
use App\Logging\WithAuthContext;
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
return [
// ...
'channels' => [
// ...
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'tap' => [WithAuthContext::class],
],
// ...
],
];
There is a way, but it is not pretty. You can create a custom monolog logger driver. The process is described at https://laravel.com/docs/8.x/logging#creating-monolog-handler-channels.
Here's a possible implementation:
class ContextEnrichingLogger extends \Monolog\Handler\AbstractHandler {
private $logger;
public function __construct($level = Monolog\Logger::DEBUG, bool $bubble = true, $underlyingLogger = 'single') {
$this->logger = Log::driver($underlyingLogger);
}
public function handle(array $record) {
$record['context'] += LogHelper::context();
return $this->logger->handle($record);
}
}
Then register this as a custom logger in your config/logging.php:
return [
'default' => 'enriched',
//...
'channels' => [
// ...
'enriched' => [
'driver' => 'monolog',
'handler' => ContextEnrichingLogger::class,
'level' => env('APP_LOG_LEVEL', 'debug'),
"with" => [
"underlyingLogger" => env('LOG_CHANNEL', 'single')
]
]
]
];
I haven't tested this particular one but this is how I've defined other custom loggers.
Note, this is probably also achievable via a custom formatter though I think it's probably the same trouble.
Code that i used and need to update for V10
$this->feUser = EidUtility::initFeUser();
When using the following code (a random) controller, the context gives me the correct login feUser object.
$context = GeneralUtility::makeInstance(Context::class);
$user = $context->getAspect('frontend.user');
DebuggerUtility::var_dump($user);
When using the same code in an eID_include class No userObject is given.
Specificly in the following class
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['FileDumpEID.php']['checkFileAccess']['xxx'] = My\Class\Hooks\FileDumpHook:class
Is there a need of bootstrapping context?
Since the TYPO3\CMS\Frontend\Middleware\EidHandler middleware is executed before the TYPO3\CMS\Frontend\Middleware\FrontendUserAuthenticator middleware in the middlewares order i dont think, that this is possible.
If you need parts of the frontend handling you either can add an own middleware with depend of TYPO3\CMS\Frontend\Middleware\FrontendUserAuthenticator or use an page Object in typoscript.
I had the same problem. You may change the order of Middlewares: https://docs.typo3.org/m/typo3/reference-coreapi/master/en-us/ApiOverview/RequestHandling/Index.html
I've created a new file RequestMiddlewares.php inside the "Configuration" directory of my extension:
<?php
return [
'frontend' => [
'typo3/cms-frontend/eid' => [
'disabled' => true
],
'typo3/cms-frontend/eid-new' => [
'target' => \TYPO3\CMS\Frontend\Middleware\EidHandler::class,
'after' => [
'typo3/cms-frontend/tsfe',
],
'before' => [
'typo3/cms-frontend/prepare-tsfe-rendering',
]
]
]
];
You have to flush TYPO3 and PHP Cache and check the ordering in "Configuration" backend module (select "HTTP Middlewares (PSR-15)").
With this setup it is possible to get the context property 'frontent.user'
$context = GeneralUtility::makeInstance(Context::class);
if($context->getPropertyFromAspect('frontend.user', 'isLoggedIn')) {
I am using this library https://github.com/yiioverflow/yii2-imap
'imap' => [
'class' => 'roopz\imap\Imap',
'connection' => [
'imapPath' => '{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX',
'imapLogin' => 'abc#gmail.com',//set this value dynamically
'imapPassword' => '123',//set this value dynamically
'serverEncoding' => 'encoding', // utf-8 default.
'attachmentsDir' => 'uploads/attachments'
],
],
//Create imap class object
$mailbox = yii::$app->imap->connection;
// Read all messaged into an array:
$mailsIds = $mailbox->searchMailbox('ALL');
in controller. want to set this value with help of session in yii2.
I found alternative php-imap library here [PHP IMAP][1]
[1]: https://github.com/barbushin/php-imap. which can be easily install with composer in yii2. and can pass a dynamic value
$mailbox = new PhpImap\Mailbox('{imap.gmail.com:993/imap/ssl}INBOX', 'some#gmail.com', '*********', __DIR__);
// Read all messaged into an array:
$mailsIds = $mailbox->searchMailbox('ALL');
Passing dynamic values that depends on Yii::$app right in config will not work because you are referring to application, and it's constructed using that config (components is a part of application too) and doesn't exist at this moment. It needs to be set later, when application is initialized and Yii::$app object exists. For example, in controller or some custom component.
Using library yiioverflow/yii2-imap it can be done like this:
use Yii;
...
Yii:$app->imap->connection = [
'imapPath' => '{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX',
'imapLogin' => $imapLogin, // Set this value dynamically
'imapPassword' => $imapPassword, // Set this value dynamically
'serverEncoding' => 'encoding', // utf-8 default
'attachmentsDir' => 'uploads/attachments',
],
Then you need to call:
Yii:$app->imap->createConnection();
to properly update the config.
There is no way to set separately imapLogin or imapPassword because of the way this component is written (these properties are protected and filled from connection array). If you want to do that, you have to subclass this component and write these setters by yourself and replace used component with your custom one.
More info about application components can be found in official docs.
You can use own "service layer" (that works similar to global Yii::$app). Just create \yii\di\ServiceLocator instance:
// Init service layer.
$services = new ServiceLocator();
$services->setComponents([
'imap' => [
'class' => 'roopz\imap\Imap',
'connection' => [
'imapPath' => '{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX',
'imapLogin' => 'abc#gmail.com',//set this value dynamically
'imapPassword' => '123',//set this value dynamically
'serverEncoding' => 'encoding', // utf-8 default.
'attachmentsDir' => 'uploads/attachments'
],
],
// ...
]);
// Retrieving the defined components:
$imap = $services->get('imap');
$imap = $services->imap;
If imap component will use only your controller, you can store $services as protected/private property of this controller.
Described approach works completely similarly to usual components in Yii::$app, because application class is also ServiceLocator.
Alternatively, you can define or redefine your component using imap-instance:
// Preparing components
$defaultImapConfig = [
'connection' => [
'imapPath' => '{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX',
'imapLogin' => null,
'imapPassword' => null,
'serverEncoding' => 'encoding', // utf-8 default.
'attachmentsDir' => 'uploads/attachments'
],
];
// Init service layer.
$services = new ServiceLocator();
// Define component
$imap = new \roopz\imap\Imap(ArrayHelper::merge($defaultImapConfig, ['connection' => [
'imapLogin' => 'abc#gmail.com',
'imapPassword' => '123',
]]));
$services->set('imap', $imap);
// Redefine component with new config
$imap = new \roopz\imap\Imap(ArrayHelper::merge($defaultImapConfig, ['connection' => [
'imapLogin' => 'dfg#gmail.com',
'imapPassword' => '456',
]]));
$services->set('imap', $imap); // If component definition with the same name already exist, it will be ovewritten.
Of course, you can use similar way to redefine global components in Yii::$app, but it is bad practice. I recommend to create separate (local) service layer, that can be accessed from your controllers, models etc.
More details about work with service locators you can found here.
I'm developing a solution that i need use Amazon WebServices library. Their library use namespace in all the project and how i am a beginner in PHP development i need your help to understand better how it works.
Here is my class:
<?php
// include('AmazonSNS\vendor\aws\aws-sdk-php\src\Sdk.php');
// include('AmazonSNS\model\CustomCredentials.php');
use Aws\Sdk;
class AwsSns {
public $sns;
public $platformApplicationArn;
public function __construct(){
$sdk = new Sdk([
'version' => 'latest',
'debug' => false,
'retries' => 3,
'credentials' => [
'key' => CustomCredentials::SNS_KEY,
'secret' => CustomCredentials::SNS_SECRET
],
'Sns' => [
'region' => 'sa-east-1'
]
]);
$this->sns = $sdk->createSns();
$this->generatePlatformApplicationArn();
}
private function generatePlatformApplicationArn( ){
$result = $this->sns->createPlatformApplication( array(
// Name is required
'Name' => 'GCMPedro',
// Platform is required
'Platform' => 'GCM',
// Attributes is required
'Attributes' => array(
// Associative array of custom 'String' key names
'PlatformCredential' => "AIzaSyBYjNaE7ShuLc2y4mf53bVwszDt8XA-YTI" //__API_KEY__
),
));
$this->platformApplicationArn = $result->get('PlatformApplicationArn');
Util::generateFile('PlataformApplicationArn: '.$this->platformApplicationArn, 'a');
}
public function getEndpointArn( $token ){
$result = $this->sns->createPlatformEndpoint(array(
// PlatformApplicationArn is required
'PlatformApplicationArn' => $this->platformApplicationArn,
// Token is required
'Token' => $token,
//'CustomUserData' => 'string',
'Attributes' => array(
// Associative array of custom 'String' key names
'Enabled' => 'true'
),
));
Util::generateFile('EndpointArn: '.$result->get('EndpointArn'), 'a');
return( $result->get('EndpointArn') );
}
}
?>
1) About name space, to use it, Do have I include or not include the .php file?
Observation:
When i don't use the include, the php returns the following error message:
Fatal error: Class 'Aws\Sdk' not found in C:\Program Files
(x86)\VertrigoServ\www\AmazonSNS\extra\AwsSns.php on line 14
Sure of your attention i thank you so much.
When you haven't set up an autoloading like PSR-0 or PSR-4 (like in the common PHP frameworks is used) or something else, the neccessary file is not going to be loaded/included automatically when its being called. I guess you haven't such an autoloading set up, so you can include with the include keyword.
In the official documentation of PHP you can read all about namespaces.
Citate of the manual. 2 benefits:
In the PHP world, namespaces are designed to solve two problems that authors of libraries and applications encounter when creating re-usable code elements such as classes or functions:
Name collisions between code you create, and internal PHP classes/functions/constants or third-party classes/functions/constants.
Ability to alias (or shorten) Extra_Long_Names designed to alleviate the first problem, improving readability of source code.
I'm working on a shopping cart (Cart model). One of its protected properties is "_items", which holds an array of Product objects. They (Products) all get stored in DB for populating the session (using ZF, Zend_Session_SaveHandler_DbTable() etc.).
public function addItem(Model_Product $product, $qty)
{
$qty = (int) $qty;
$pId = $product->getId();
if ($qty > 0) {
$this->_items[$pId] = array('product' => $product, 'qty' => $qty);
} else {
// if the quantity is zero (or less), remove item from stack
unset($this->_items[$pId]);
}
// add new info to session
$this->persist();
}
In the controller, I grab a Product obj from DB with the ProductMapper and provide it to "addItem()":
$product1 = $prodMapper->getProductByName('cap');
$this->_cart->addItem($product1, 2);
getProductByName() returns a new populated Model_Product object.
I usually get the
Please ensure that the class definition "Model_Product" of the object you are trying to operate on was loaded _before_ ...
error message, a session dump obviously shows
['__PHP_Incomplete_Class_Name'] => 'Model_Product'
I know about the "declaring the class before serializing it". My problem is this: how can I declare the Product class in addItem(), if it's injected (first param) in the first place? Wouldn't a new declaration (like new Model_Product()) overwrite the param (original object) in addItem()? Must I declare it in the Cart model again?
Besides, I'll surely get a Cannot redeclare class Model_Product if I... redeclare it in Cart.
In ZF's bootstrap, the session was started before autoloading.
/**
* Make XXX_* classes available
*/
protected function _initAutoloaders()
{
$loader = new Zend_Application_Module_Autoloader(array(
'namespace' => 'XXX',
'basePath' => APPLICATION_PATH
));
}
public function _initSession()
{
$config = $this->_config->custom->session;
/**
* For other settings, see the link below:
* http://framework.zend.com/manual/en/zend.session.global_session_management.html
*/
$sessionOptions = array(
'name' => $config->name,
'gc_maxlifetime' => $config->ttl,
'use_only_cookies' => $config->onlyCookies,
// 'strict' => true,
// 'path' => '/',
);
// store session info in DB
$sessDbConfig = array(
'name' => 'xxx_session',
'primary' => 'id',
'modifiedColumn' => 'modified',
'dataColumn' => 'data',
'lifetimeColumn' => 'lifetime'
);
Zend_Session::setOptions($sessionOptions);
Zend_Session::setSaveHandler(new Zend_Session_SaveHandler_DbTable($sessDbConfig));
Zend_Session::start();
}
When I was getting the errors I was talking about, the method declaration was the other way around: _initSession() was first, then _initAutoloaders() - and this was the exact order ZF was processing them.
I'll test some more, but this seems to work (and logical). Thanks for all your suggestions.