I am writting a console application with Symfony2 components, and I want to add distinct logging channels for my services, my commands and so on. The problem: to create a new channel requires to create a new instance of Monolog, and I don't really know how to handle this in a generic way, and without needing to pass the stream handler, a channel and the proper code to bind the one and the other inside all services.
I did the trick using debug_backtrace():
public function log($level, $message, array $context = array ())
{
$trace = array_slice(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3), 1);
$caller = $trace[0]['class'] !== __CLASS__ ? $trace[0]['class'] : $trace[1]['class'];
if (!array_key_exists($caller, $this->loggers))
{
$monolog = new Monolog($caller);
$monolog->pushHandler($this->stream);
$this->loggers[$caller] = $monolog;
}
$this->loggers[$caller]->log($level, $message, $context);
}
Whatever from where I call my logger, it creates a channel for each class that called it. Looks cool, but as soon as a logger is called tons of time, this is performance-killing.
So here is my question:
Do you know a better generic way to create one distinct monolog channel per class that have a logger property?
The above code packaged for testing:
composer.json
{
"require" : {
"monolog/monolog": "~1.11.0"
}
}
test.php
<?php
require('vendor/autoload.php');
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
class Test
{
public function __construct($logger)
{
$logger->info("test!");
}
}
class Hello
{
public function __construct($logger)
{
$logger->log(Monolog\Logger::ALERT, "hello!");
}
}
class LeveragedLogger implements \Psr\Log\LoggerInterface
{
protected $loggers;
protected $stream;
public function __construct($file, $logLevel)
{
$this->loggers = array ();
$this->stream = new StreamHandler($file, $logLevel);
}
public function alert($message, array $context = array ())
{
$this->log(Logger::ALERT, $message, $context);
}
public function critical($message, array $context = array ())
{
$this->log(Logger::CRITICAL, $message, $context);
}
public function debug($message, array $context = array ())
{
$this->log(Logger::DEBUG, $message, $context);
}
public function emergency($message, array $context = array ())
{
$this->log(Logger::EMERGENCY, $message, $context);
}
public function error($message, array $context = array ())
{
$this->log(Logger::ERROR, $message, $context);
}
public function info($message, array $context = array ())
{
$this->log(Logger::INFO, $message, $context);
}
public function log($level, $message, array $context = array ())
{
$trace = array_slice(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3), 1);
$caller = $trace[0]['class'] !== __CLASS__ ? $trace[0]['class'] : $trace[1]['class'];
if (!array_key_exists($caller, $this->loggers))
{
$monolog = new Logger($caller);
$monolog->pushHandler($this->stream);
$this->loggers[$caller] = $monolog;
}
$this->loggers[$caller]->log($level, $message, $context);
}
public function notice($message, array $context = array ())
{
$this->log(Logger::NOTICE, $message, $context);
}
public function warning($message, array $context = array ())
{
$this->log(Logger::WARNING, $message, $context);
}
}
$logger = new LeveragedLogger('php://stdout', Logger::DEBUG);
new Test($logger);
new Hello($logger);
Usage
ninsuo:test3 alain$ php test.php
[2014-10-21 08:59:04] Test.INFO: test! [] []
[2014-10-21 08:59:04] Hello.ALERT: hello! [] []
What would you think about making the decision which logger should be used right before the consumers are created? This could be easily accomplished with some kind of DIC or maybe a factory.
<?php
require('vendor/autoload.php');
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Psr\Log\LoggerInterface;
use Monolog\Handler\HandlerInterface;
class Test
{
public function __construct(LoggerInterface $logger)
{
$logger->info("test!");
}
}
class Hello
{
public function __construct(LoggerInterface $logger)
{
$logger->log(Monolog\Logger::ALERT, "hello!");
}
}
class LeveragedLoggerFactory
{
protected $loggers;
protected $stream;
public function __construct(HandlerInterface $streamHandler)
{
$this->loggers = array();
$this->stream = $streamHandler;
}
public function factory($caller)
{
if (!array_key_exists($caller, $this->loggers)) {
$logger = new Logger($caller);
$logger->pushHandler($this->stream);
$this->loggers[$caller] = $logger;
}
return $this->loggers[$caller];
}
}
$loggerFactory = new LeveragedLoggerFactory(new StreamHandler('php://stdout', Logger::DEBUG));
new Test($loggerFactory->factory(Test::class));
new Hello($loggerFactory->factory(Hello::class));
I finally created a MonologContainer class that extends the standard Symfony2 container, and injects a Logger to LoggerAware services. Overloading the get() method of the service container, I can get the service's ID, and use it as a channel for the logger.
<?php
namespace Fuz\Framework\Core;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Monolog\Handler\HandlerInterface;
use Monolog\Logger;
use Psr\Log\LoggerAwareInterface;
class MonologContainer extends ContainerBuilder
{
protected $loggers = array ();
protected $handlers = array ();
protected $processors = array ();
public function __construct(ParameterBagInterface $parameterBag = null)
{
parent::__construct($parameterBag);
}
public function pushHandler(HandlerInterface $handler)
{
foreach (array_keys($this->loggers) as $key)
{
$this->loggers[$key]->pushHandler($handler);
}
array_unshift($this->handlers, $handler);
return $this;
}
public function popHandler()
{
if (count($this->handlers) > 0)
{
foreach (array_keys($this->loggers) as $key)
{
$this->loggers[$key]->popHandler();
}
array_shift($this->handlers);
}
return $this;
}
public function pushProcessor($callback)
{
foreach (array_keys($this->loggers) as $key)
{
$this->loggers[$key]->pushProcessor($callback);
}
array_unshift($this->processors, $callback);
return $this;
}
public function popProcessor()
{
if (count($this->processors) > 0)
{
foreach (array_keys($this->loggers) as $key)
{
$this->loggers[$key]->popProcessor();
}
array_shift($this->processors);
}
return $this;
}
public function getHandlers()
{
return $this->handlers;
}
public function get($id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE)
{
$service = parent::get($id, $invalidBehavior);
return $this->setLogger($id, $service);
}
public function setLogger($id, $service)
{
if ($service instanceof LoggerAwareInterface)
{
if (!array_key_exists($id, $this->loggers))
{
$this->loggers[$id] = new Logger($id, $this->handlers, $this->processors);
}
$service->setLogger($this->loggers[$id]);
}
return $service;
}
}
Usage example:
test.php
#!/usr/bin/env php
<?php
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Fuz\Framework\Core\MonologContainer;
if (!include __DIR__ . '/vendor/autoload.php')
{
die('You must set up the project dependencies.');
}
$container = new MonologContainer();
$loader = new YamlFileLoader($container, new FileLocator(__DIR__));
$loader->load('services.yml');
$handler = new StreamHandler(__DIR__ ."/test.log", Logger::WARNING);
$container->pushHandler($handler);
$container->get('my.service')->hello();
services.yml
parameters:
my.service.class: Fuz\Runner\MyService
services:
my.service:
class: %my.service.class%
MyService.php
<?php
namespace Fuz\Runner;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
class MyService implements LoggerAwareInterface
{
protected $logger;
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function hello()
{
$this->logger->alert("Hello, world!");
}
}
Demo
ninsuo:runner alain$ php test.php
ninsuo:runner alain$ cat test.log
[2014-11-06 08:18:55] my.service.ALERT: Hello, world! [] []
You can try this
<?php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\FirePHPHandler;
class Loggr{
private static $_logger;
public $_instance;
public $_channel;
private function __construct(){
if(!isset(self::$_logger))
self::$_logger = new Logger('Application Log');
}
// Create the logger
public function logError($error){
self::$_logger->pushHandler(new StreamHandler(LOG_PATH . 'application.'. $this->_channel . '.log', Logger::ERROR));
self::$_logger->addError($error);
}
public function logInfo($info){
self::$_logger->pushHandler(new StreamHandler(LOG_PATH . 'application.'. $this->_channel . '.log', Logger::INFO));
self::$_logger->addInfo($info);
}
public static function getInstance($channel) {
$_instance = new Loggr();
$_instance->_channel = strtolower($channel);
return $_instance;
}
}
and can be consumed as
class LeadReport extends Controller{
public function __construct(){
$this->logger = Loggr::getInstance('cron');
$this->logger->logError('Error generating leads');
}
}
Related
I am running phpunit version 9.2 and I would like to know why my method is not covered in the phpunit coverage.
This is my class:
class Methods extends Template
{
const DISABLED_PAYMENT_METHODS_1 = 'free';
const DISABLED_PAYMENT_METHODS_2 = 'adyen_cc';
const DISABLED_PAYMENT_METHODS_3 = 'adyen_oneclick';
protected $paymentMethodList;
protected $storeManager;
protected $logger;
public function __construct(
Context $context,
PaymentMethodList $paymentMethodList,
StoreManagerInterface $storeManager,
LoggerInterface $logger,
array $data = []
) {
$this->paymentMethodList = $paymentMethodList;
$this->storeManager = $storeManager;
$this->logger = $logger;
parent::__construct($context, $data);
}
public function getPaymentMethods()
{
try {
$storeId = $this->storeManager->getStore()->getId();
$paymentList = $this->paymentMethodList->getActiveList($storeId);
$resultPayments = [];
foreach ($paymentList as $payment) {
if ($payment->getCode() !== self::DISABLED_PAYMENT_METHODS_1 &&
$payment->getCode() !== self::DISABLED_PAYMENT_METHODS_2 &&
$payment->getCode() !== self::DISABLED_PAYMENT_METHODS_3
) {
$resultPayments[] = $payment;
}
}
return $resultPayments;
} catch (Exception $e) {
$this->logger->error($e->getMessage());
return false;
}
}
}
and this is my test class:
class MethodsTest extends TestCase
{
private $model;
private function getSimpleMock($originalClassName)
{
return $this->getMockBuilder($originalClassName)
->disableOriginalConstructor()
->getMock();
}
public function setUp() : void
{
$context = $this->getSimpleMock(Context::class);
$paymentMethodList = $this->getSimpleMock(PaymentMethodList::class);
$storeManager = $this->getSimpleMock(StoreManagerInterface::class);
$logger = $this->getSimpleMock(LoggerInterface::class);
$this->model = new Methods(
$context,
$paymentMethodList,
$storeManager,
$logger,
[]
);
}
public function testGetPaymentMethods()
{
$stub = $this->createMock(Methods::class);
$stub->method('getPaymentMethods')
->willReturn([]);
try {
$stub->getPaymentMethods();
$this->fail("Expected exception!");
} catch (\Exception $error) {
$this->assertEquals("Expected exception!", $error->getMessage());
}
}
}
When I run the command to get the coverage. I am getting:
I am really curious why my test is not covered or at least the exception part ? Would you please share you ideas why ? and what can i do in order to fix this ? Right now I got a 29 % and I would like to get at least 60% coverage.
Thank you
On this line $stub = $this->createMock(Methods::class); you are creating a mock of the Methods class, so not actually testing the real class.
You will need to use the object you created in your setUp() method, and set up mock returns on the dependencies you passed in (perhaps converting some of them to be class properties).
You should test the real class, as example:
public function testGetPaymentMethods()
{
// define a payment
$paymentFreeCode = $this->createMock(Payment::class);
$paymentFreeCode->method('getcode')
->willReturn("free");
// define a payment
$payment = $this->createMock(Payment::class);
$payment->method('getcode')
->willReturn("invalid-code");
$paymentList = [
$paymentFreeCode,
$payment,
];
// define a store
$store = $this->createMock(Store::class);
$store->method('getId')
->willReturn("my-store-id");
// return store from the store manager
$this->storeManager->method('getStore')
->willReturn(myStore);
// return the payment list
$this->paymentMethodList->method('getActiveList')->with("my-store-id")
->willReturn($paymentList);
// call the real class instrumented with mocks
$paymentMethods = $this->model->getPaymentMethods();
$this->assertIsArray($paymentMethods);
$this->assertCount($paymentMethods, 1);
}
I am trying to display an array of messages at the end of my PHP class. My message handler is working, but only if I "add_message" from within the main parent class and not if I call this function from within a child class. Sorry if this is vague but was not sure how to word the question.
TLDR; How can I add a message from within class Example?
MAIN PARENT CLASS
class Init {
public function __construct() {
$this->load_dependencies();
$this->add_messages();
$this->add_msg_from_instance();
}
private function load_dependencies() {
require_once ROOT . 'classes/class-messages.php';
require_once ROOT . 'classes/class-example.php';
}
public function add_messages() {
$this->messages = new Message_Handler();
$this->messages->add_message( 'hello world' );
}
// I Would like to add a message from within this instance....
public function add_msg_from_instance() {
$example = new Example();
$example->fire_instance();
}
public function run() {
$this->messages->display_messages();
}
}
MESSAGE HANDLER
class Message_Handler {
public function __construct() {
$this->messages = array();
}
public function add_message( $msg ) {
$this->messages = $this->add( $this->messages, $msg );
}
private function add( $messages, $msg ) {
$messages[] = $msg;
return $messages;
}
// Final Function - Should display array of all messages
public function display_messages() {
var_dump( $this->messages );
}
}
EXAMPLE CLASS
class Example {
public function fire_instance() {
$this->messages = new Message_Handler();
$this->messages->add_message( 'Hello Universe!' ); // This message is NOT being displayed...
}
}
Because you want to keep the messages around different object, you should pass the object or use a static variable.
I would use a static variable like so:
class Init {
public function __construct() {
$this->load_dependencies();
$this->add_messages();
$this->add_msg_from_instance();
}
private function load_dependencies() {
require_once ROOT . 'classes/class-messages.php';
require_once ROOT . 'classes/class-example.php';
}
public function add_messages() {
// renamed the message handler variable for clarity
$this->message_handler = new Message_Handler();
$this->message_handler->add_message( 'hello world' );
}
// I Would like to add a message from within this instance....
public function add_msg_from_instance() {
$example = new Example();
$example->fire_instance();
}
public function run() {
$this->message_handler->display_messages();
}
}
class Message_Handler {
// use a static var to remember the messages over all objects
public static $_messages = array();
// add message to static
public function add_message( $msg ) {
self::$_messages[] = $msg;
}
// Final Function - Should display array of all messages
public function display_messages() {
var_dump( self::$_messages );
}
}
class Example {
public function fire_instance() {
// new object, same static array
$message_handler = new Message_Handler();
$message_handler->add_message( 'Hello Universe!' );
}
}
// testing...
new Init();
new Init();
$init = new Init();
$init->add_msg_from_instance();
$init->add_msg_from_instance();
$init->add_msg_from_instance();
$init->run();
Although global variables might not be the best design decision, you have at least two approaches to achieve what you want:
Use singleton.
Nowadays it is considered anti-pattern, but it is the simplest way: make message handler a singleton:
class MessageHandler
{
private static $instance;
private $messages = [];
public static function instance(): self
{
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct()
{
}
public function addMessage($message): self
{
$this->messages[] = $message;
return $this;
}
public function messages(): array
{
return $this->messages;
}
}
Then instead of creating a new instance of MessageHandler access it via the static method MessageHandler::instance(). Here is a demo.
Use DI container to inject the same instance (that is created once and held in the container) into all instances that need to access it. This approach is more preferable, but harder to implement in the project where there is no DI container available in the first place.
I have a php lib which is a set of functions,Here it is
<?php
# Copyright (c) 2010-2011 Arnaud Renevier, Inc, published under the modified BSD
# license.
namespace App\Gislib;
abstract class CustomException extends \Exception {
protected $message;
public function __toString() {
return get_class($this) . " {$this->message} in {$this->file}({$this->line})\n{$this->getTraceAsString()}";
}
}
class Unimplemented extends CustomException {
public function __construct($message) {
$this->message = "unimplemented $message";
}
}
class UnimplementedMethod extends Unimplemented {
public function __construct($method, $class) {
$this->message = "method {$this->class}::{$this->method}";
}
}
class InvalidText extends CustomException {
public function __construct($decoder_name, $text = "") {
$this->message = "invalid text for decoder " . $decoder_name . ($text ? (": " . $text) : "");
}
}
class InvalidFeature extends CustomException {
public function __construct($decoder_name, $text = "") {
$this->message = "invalid feature for decoder $decoder_name" . ($text ? ": $text" : "");
}
}
abstract class OutOfRangeCoord extends CustomException {
private $coord;
public $type;
public function __construct($coord) {
$this->message = "invalid {$this->type}: $coord";
}
}
class OutOfRangeLon extends outOfRangeCoord {
public $type = "longitude";
}
class OutOfRangeLat extends outOfRangeCoord {
public $type = "latitude";
}
class UnavailableResource extends CustomException {
public function __construct($ressource) {
$this->message = "unavailable ressource: $ressource";
}
}
interface iDecoder {
/*
* #param string $text
* #return Geometry
*/
static public function geomFromText($text);
}
abstract class Decoder implements iDecoder {
static public function geomFromText($text) {
throw new UnimplementedMethod(__FUNCTION__, get_called_class());
}
}
interface iGeometry {
/*
* #return string
*/
public function toGeoJSON();
/*
* #return string
*/
public function toKML();
/*
* #return string
*/
public function toWKT();
/*
* #param mode: trkseg, rte or wpt
* #return string
*/
public function toGPX($mode = null);
/*
* #param Geometry $geom
* #return boolean
*/
public function equals(Geometry $geom);
}
abstract class Geometry implements iGeometry {
const name = "";
public function toGeoJSON() {
throw new UnimplementedMethod(__FUNCTION__, get_called_class());
}
public function toKML() {
throw new UnimplementedMethod(__FUNCTION__, get_called_class());
}
public function toGPX($mode = null) {
throw new UnimplementedMethod(__FUNCTION__, get_called_class());
}
public function toWKT() {
throw new UnimplementedMethod(__FUNCTION__, get_called_class());
}
public function equals(Geometry $geom) {
throw new UnimplementedMethod(__FUNCTION__, get_called_class());
}
public function __toString() {
return $this->toWKT();
}
}
class GeoJSON extends Decoder {
static public function geomFromText($text) {
$ltext = strtolower($text);
$obj = json_decode($ltext);
if (is_null ($obj)) {
throw new InvalidText(__CLASS__, $text);
}
try {
$geom = static::_geomFromJson($obj);
} catch(InvalidText $e) {
throw new InvalidText(__CLASS__, $text);
} catch(\Exception $e) {
throw $e;
}
return $geom;
}
static protected function _geomFromJson($json) {
if (property_exists ($json, "geometry") and is_object($json->geometry)) {
return static::_geomFromJson($json->geometry);
}
if (!property_exists ($json, "type") or !is_string($json->type)) {
throw new InvalidText(__CLASS__);
}
foreach (array("Point", "MultiPoint", "LineString", "MultiLinestring", "LinearRing",
"Polygon", "MultiPolygon", "GeometryCollection") as $json_type) {
if (strtolower($json_type) == $json->type) {
$type = $json_type;
break;
}
}
if (!isset($type)) {
throw new InvalidText(__CLASS__);
}
try {
$components = call_user_func(array('static', 'parse'.$type), $json);
} catch(InvalidText $e) {
throw new InvalidText(__CLASS__);
} catch(\Exception $e) {
throw $e;
}
$constructor = __NAMESPACE__ . '\\' . $type;
return new $constructor($components);
}
static protected function parsePoint($json) {
if (!property_exists ($json, "coordinates") or !is_array($json->coordinates)) {
throw new InvalidText(__CLASS__);
}
return $json->coordinates;
}
static protected function parseMultiPoint($json) {
if (!property_exists ($json, "coordinates") or !is_array($json->coordinates)) {
throw new InvalidText(__CLASS__);
}
return array_map(function($coords) {
return new Point($coords);
}, $json->coordinates);
}
static protected function parseLineString($json) {
return static::parseMultiPoint($json);
}
static protected function parseMultiLineString($json) {
$components = array();
if (!property_exists ($json, "coordinates") or !is_array($json->coordinates)) {
throw new InvalidText(__CLASS__);
}
foreach ($json->coordinates as $coordinates) {
$linecomp = array();
foreach ($coordinates as $coordinates) {
$linecomp[] = new Point($coordinates);
}
$components[] = new LineString($linecomp);
}
return $components;
}
static protected function parseLinearRing($json) {
return static::parseMultiPoint($json);
}
static protected function parsePolygon($json) {
$components = array();
if (!property_exists ($json, "coordinates") or !is_array($json->coordinates)) {
throw new InvalidText(__CLASS__);
}
foreach ($json->coordinates as $coordinates) {
$ringcomp = array();
foreach ($coordinates as $coordinates) {
$ringcomp[] = new Point($coordinates);
}
$components[] = new LinearRing($ringcomp);
}
return $components;
}
static protected function parseMultiPolygon($json) {
$components = array();
if (!property_exists ($json, "coordinates") or !is_array($json->coordinates)) {
throw new InvalidText(__CLASS__);
}
foreach ($json->coordinates as $coordinates) {
$polycomp = array();
foreach ($coordinates as $coordinates) {
$ringcomp = array();
foreach ($coordinates as $coordinates) {
$ringcomp[] = new Point($coordinates);
}
$polycomp[] = new LinearRing($ringcomp);
}
$components[] = new Polygon($polycomp);
}
return $components;
}
static protected function parseGeometryCollection($json) {
if (!property_exists ($json, "geometries") or !is_array($json->geometries)) {
throw new InvalidText(__CLASS__);
}
$components = array();
foreach ($json->geometries as $geometry) {
$components[] = static::_geomFromJson($geometry);
}
return $components;
}
}}
I have placed it in App\Gislib\Gislib.php
and in my controller I have added its using as use App\Gislib\GeoJSON; but when I try to load its class $decoder =new \App\Gislib\GeoJSON(); it says Class 'App\Gislib\GeoJSON' not found where is my mistake?Is it related to extended types or namespaces? I know there are some other methods to call these classes but I just can load them using namespaces
thanks
Each class or interface needs to be in its own file, with the file name matching the class name.
For example, in app/Gislib/Unimplemented.php:
<?php
namespace App\Gislib;
class Unimplemented extends CustomException {
public function __construct($message) {
$this->message = "unimplemented $message";
}
}
and then in app/Gislib/iDecoder.php:
<?php
namespace App\Gislib;
interface iDecoder {
/*
* #param string $text
* #return Geometry
*/
static public function geomFromText($text);
}
This is due to Laravel following PSR-4 standards.
If you still get the error after splitting the file up, try running composer dump.
With PSR-4 autoloading it is required that the class name matches the file name. See https://stackoverflow.com/a/29033779 .
But what you can do is to modify your composer.json file like this:
"autoload": {
"classmap": [
"database/seeds",
"database/factories"
],
"files" : [
"app/Gislib/Gislib.php"
],
"psr-4": {
"App\\": "app/"
}
},
Add as section "files" and provide the path to your lib. ( I think you have to do composer dump-autoload after that. Now it should work.
I have a class with only one function:
<?php
class EventLog
{
public function logEvent($data, $object, $operation, $id)
{
//Log it to a file...
$currentTime = new DateTime();
$time = $currentTime->format('Y-m-d H:i:s');
$logFile = "/.../event_log.txt";
$message = "Hello world";
//Send the data to a file...
file_put_contents($logFile, $message, FILE_APPEND);
}
}
Then I have another class with many functions and each and everyone need to call the above method. To instantiate the class in every function I have done:
$log = new EventLog();
//Then...
$log->logEvent($data, $object, $operation, $id);
The problem: I have used the above code in every function and what I would like to know is if there is a way to instantiate the EventLog class once for all the functions that need it.
You can create single instance at the beginning(for example) of your script and inject it into constructors of those classes that need it. This is called Dependency Injection. Most PHP web frameworks utilize this principle.
class Logger
{
public function writeToLogFile(){
...
}
}
class DoSomethingUseful
{
private $logger;
public function __construct(Logger $logger) //php 7 optional type hinting
{
$this->logger = $logger;
}
public function actualWork()
{
//do work
$this->logger->writeToLogFile('whatever');
}
}
class Application
{
public function setUp()
{
//create database connection, other stuff
$this->logger = new Logger;
}
public function work()
{
$action = new DoSomethingUseful($this->logger);
$action->actualWork();
}
}
You could also try using PHP Trait (with namespacing):
<?php
namespace App\Traits;
trait EventLog
{
public function logEvent($data, $object, $operation, $id)
{
//Log it to a file...
$currentTime = new DateTime();
$time = $currentTime->format('Y-m-d H:i:s');
$logFile = "/.../event_log.txt";
$message = "Hello world";
//Send the data to a file...
file_put_contents($logFile, $message, FILE_APPEND);
}
}
In your other class:
<?php
namespace App;
// import your trait here
use App\Traits\EventLog;
class OtherClass
{
use EventLog;
public function sample() {
// sample call to log event
$this->logEvent($data, $object, $operation, $id);
}
}
I code a class for Hook system. But this is outdated. I want to use splObserver to code it.
<?php
class Event
{
private static $filters = [];
private static $actions = [];
public static function addAction($name, $callback, $priority = 10)
{
if (! isset(static::$actions[$name])) {
static::$actions[$name] = [];
}
static::$actions[$name][] = [
'priority' => (int)$priority,
'callback' => $callback,
];
}
public function doAction($name, ...$args)
{
$actions = isset(static::$actions[$name]) ? static::$actions[$name] : false;
if (! $actions) {
return;
}
// sort actions by priority
$sortArr = array_map(function ($action) {
return $action['priority'];
}, $actions);
\array_multisort($sortArr, $actions);
foreach ($actions as $action) {
\call_user_func_array($action['callback'], $args);
}
}
}
Event::addAction('action1', function(){
echo 'balabala1';
});
Event::addAction('action1', function(){
echo 'balabala2';
});
Event::doAction('action1');
Output: balabala1 balabala2
It works good.
I want to use SplObserver to re-code it and try to code but no idea.
I don't really know whether this implementation could be useful in a real life application or not but, for the sake of answering your question, here we go...
Let's imagine we have a User class that we'd like to hook with our custom functions.
First, we create a reusable trait containing the Subject logic, capable of managing "event names" to whom we can hook our actions.
trait SubjectTrait {
private $observers = [];
// this is not a real __construct() (we will call it later)
public function construct()
{
$this->observers["all"] = [];
}
private function initObserversGroup(string $name = "all")
{
if (!isset($this->observers[$name])) {
$this->observers[$name] = [];
}
}
private function getObservers(string $name = "all")
{
$this->initObserversGroup($name);
$group = $this->observers[$name];
$all = $this->observers["all"];
return array_merge($group, $all);
}
public function attach(\SplObserver $observer, string $name = "all")
{
$this->initObserversGroup($name);
$this->observers[$name][] = $observer;
}
public function detach(\SplObserver $observer, string $name = "all")
{
foreach ($this->getObservers($name) as $key => $o) {
if ($o === $observer) {
unset($this->observers[$name][$key]);
}
}
}
public function notify(string $name = "all", $data = null)
{
foreach ($this->getObservers($name) as $observer) {
$observer->update($this, $name, $data);
}
}
}
Next, we use the trait in our SplSubject User class:
class User implements \SplSubject
{
// It's necessary to alias construct() because it
// would conflict with other methods.
use SubjectTrait {
SubjectTrait::construct as protected constructSubject;
}
public function __construct()
{
$this->constructSubject();
}
public function create()
{
// User creation code...
$this->notify("User:created");
}
public function update()
{
// User update code...
$this->notify("User:updated");
}
public function delete()
{
// User deletion code...
$this->notify("User:deleted");
}
}
The last step is to implement a reusable SplObserver. This observer is able to bind himself to a Closure (anonymous function).
class MyObserver implements SplObserver
{
protected $closure;
public function __construct(Closure $closure)
{
$this->closure = $closure->bindTo($this, $this);
}
public function update(SplSubject $subject, $name = null, $data = null)
{
$closure = $this->closure;
$closure($subject, $name, $data);
}
}
Now, the test:
$user = new User;
// our custom functions (Closures)
$function1 = function(SplSubject $subject, $name, $data) {
echo $name . ": function1\n"; // we could also use $data here
};
$function2 = function(SplSubject $subject, $name, $data) {
echo $name . ": function2\n";
};
// subscribe the first function to all events
$user->attach(new MyObserver($function1), 'all');
// subscribe the second function to user creations only
$user->attach(new MyObserver($function2), 'User:created');
// run a couple of methods to see what happens
$user->create();
$user->update();
The output will be:
User:created: function2
User:created: function1
User:updated: function1
NOTE: we could use SplObjectStorage instead of an array, to store observers in the trait.