I am looking for a way to extend our default logging class without making changes to the whole application or library. We have a number of places where we write logs. E.g:
App_Log::getInstance()->write(
$name,
$type,
"LOGOUT",
$url
);
Auth_Log
<?php
class App_Auth_Log {
/**
* Singleton instance
*
* Marked only as protected to allow extension of the class. To extend,
* simply override {#link getInstance()}.
*
* #var App_Auth_Log
*/
protected static $_instance = null;
/**
* Auth logging enabled flag.
*
* #var boolean
*/
protected $_enabled = false;
/**
* If flag is true then cleanup will not remove login records.
*
* #var boolean
*/
protected $_keepLoginRecords = false;
/**
* Class constructor.
*/
public function __construct() {
if(App_Front::getInstance()->hasParam("withAuthLog"))
$this->_enabled = true;
if(App_Front::getInstance()->hasParam("withKeepLoginRecords"))
$this->_keepLoginRecords = true;
$this->cleanup();
}
/**
* Singleton instance
*
* #return App_Auth_Log
*/
public static function getInstance() {
if (is_null(self::$_instance))
self::$_instance = new self();
return self::$_instance;
}
/**
* Write new auth log record with given details. if succesful then method
* returns true otherwise returns false.
*
* #param string $class
* #param string $ident
* #param string $action
* #param string $url
* #param string $ipaddr
* #return boolean
* #throws Exception
*/
public function write($class,$ident,$action,$url,$ipaddr=null) {
if($this->isEnabled()) {
$db = App_Db_Connections::getInstance()->getConnection();
try {
// if address not specificed get remote addr
$ipaddr = ($ipaddr == null) ? $_SERVER['REMOTE_ADDR'] : $ipaddr;
// manual insert so we can take advantage of insert delayed
$stmnt = "INSERT INTO accesslogs
VALUES('',NOW(),'$class','$ident','$action','$url','$ipaddr')";
// execute insert
$db->query($stmnt);
} catch (Exception $e) {
throw $e;
}
return true;
}
return false;
}
/**
* Cleanup old accesslog records. Cached to run once a day.
*
* #return boolean - returns true if run false if not.
*/
public function cleanup() {
$cache = App_Cache::getInstance()->newObject(86400);
if($this->isEnabled()) {
if (!$res = $cache->load(App_Cache::getCacheName(__CLASS__. "cleanup"))) {
// add cache
$db = App_Db_Connections::getInstance()->getConnection();
try {
$where = $db->quoteInto("DATEDIFF(NOW(),accesslog_datetime) > ?", 6);
$and = ($this->_keepLoginRecords) ? " AND accesslog_action != 'LOGIN'" : "";
$db->query("DELETE LOW_PRIORITY FROM accesslogs WHERE $where $and");
} catch (Exception $e) {
throw $e;
}
$cache->save($res,App_Cache::getCacheName(__CLASS__. "cleanup"));
} // end cache
}
return;
}
/**
* Returns boolean check if auth log enabled.
*
* #return boolean
*/
public function isEnabled() {
return ($this->_enabled) ? true : false;
}
/**
* Enabled disable the auth log process.
*
* #param boolean $boolean
* #return App_Auth_Log
*/
public function setEnabled($boolean) {
$this->_enabled = ($boolean) ? true : false;
return $this;
}
}
?>
This is the default behaviour of the core code. But for this specific project I need to be able to extend/overwrite the write method, e.g with extra parameters.
Q: How can I make changes to this App_Auth_Log class so that its backwards compatible with previous projects that call App_Log::getInstance()->write?
How I think it should work(but dont know how to do it).
If App_Front::getInstance()->hasParam("withAuthLog") passes a custom class name e.g: My_Custom_Auth_Log which overwrites the original write method. Just not sure how to modify the singleton part
You have no choice. You have to modify your App_Log code, because everything statically makes calls to it. For minimal changes, you could extend the App_Log class and make App_Log::getInstance return an instance of that child class; but that's pretty messy, since a parent should never know about its children.
Maybe you can prevent the default implementation of App_Log to be loaded and load a different implementation of it from a different file. That's pretty messy too though.
This is exactly (one of) the reason(s) why singletons and static calls are very frowned upon. See How Not To Kill Your Testability Using Statics.
Related
currently i have a problem which don't allow me to continue adding features to my mvc website without do any sort of spaghetti code.
i have two classes, one is ModModel and the other is ModUploadModel. both are extended with the Model class.
ModModel contains all the methods about "mods", as ModModel->doesModNameExists(), ModModel->getModDetails() etc...
ModUploadModel contains all the methods for the uploading of a mod, as ModUploadModel->upload(), ModUploadModel->isModNameValid() etc...
in some cases i have to call some ModModel methods from ModUploadModel, and to do so i have to create a new instance of ModModel inside the ModUploadController and to pass it as an argument to ModUploadModel->upload().
for example: the ModUploadController creates two new objects, $modModel = new ModModel() and $modUploadModel = new ModUploadModel(), then calls $modUploadModel->upload($modModel).
this is the ModUploadController, which creates the two objects and call the ModUploadModel->upload() method
class ModUploadController extends Mvc\Controller {
public function uploadMod(): void {
$modUploadModel = new ModUploadModel()
$modModel = new ModModel();
// $modModel needs to be passed because the ModUploadModel needs
// one of its methods
if ($modUploadModel->upload("beatiful-mod", $modModel)) {
// success
} else {
// failure
}
}
}
ModUploadModel->upload() checks if the input is valid (if the mod name isn't already taken etc), and finally upload the mod data into the db. obviously it's all suddivise in more sub private methods, as ModUploadModel->isModNameValid() and ModUploadModel->insertIntoDb().
the problem is that i don't structured my classes with all static methods, and everytime i have to pass objects as parameters, like with ModModel (for example i need its isModNameValid() method).
i thought about making all the ModModel methods static, but that's not as simple as it seems, because all its methods query the db, and they use the Model->executeStmt() method (remember that all the FooBarModel classes are extended with the Model class, which contains usefull common methods as executeStmt() and others), and calling a non static method from a static one is not a good practice in php, so i should make static the Model methods too, and consequently also the Dbh methods for the db connection (Model is extended with Dbh).
the ModModel class:
class ModModel extends Mvc\Model {
// in reality it queries the db with $this->executeStmt(),
// which is a Model method
public function doesModNameExists($name) {
if (/* exists */) {
return true;
}
return false;
}
}
the ModUploadModel class:
class ModUploadModel extends Mvc\Model {
private $modName;
public function upload($modName, $modModel) {
$this->modName = $modName;
if (!$this->isModNameValid($modModel)) {
return false;
}
if ($this->insertIntoDb()) {
return true;
}
return false;
}
// this methods needs to use the non static doesModNameExists() method
// which is owned by the ModModel class, so i need to pass
// the object as an argument
private function isModNameValid($modModel) {
if ($modModel->doesModNameExists($this->modName)) {
return false;
}
// other if statements
return true;
}
private function insertIntoDb() {
$sql = "INSERT INTO blabla (x, y) VALUES (?, ?)";
$params = [$this->modName, "xxx"];
if ($this->executeStmt($sql, $params)) {
return true;
}
return false;
}
}
the alternative would be to create a new instance of Model inside the ModModel methods, for example (new Model)->executeStmt(). the problem is that it's not a model job to create new objects and generally it's not the solution i like most.
Some observations and suggestions:
[a] You are passing a ModModel object to ModUploadModel to validate the mod name before uploading. You shouldn't even try to call ModUploadModel::upload() if a mod with the provided name already exists. So you should follow steps similar to this:
class ModUploadController extends Mvc\Controller {
public function uploadMod(): void {
$modUploadModel = new ModUploadModel()
$modModel = new ModModel();
$modName = 'beatiful-mod';
try {
if ($modModel->doesModNameExists($modName)) {
throw new \ModNameExistsException('A mod with the name "' . $modName . '" already exists');
}
$modUploadModel->upload($modName);
} catch (\ModNameExistsException $exception){
// ...Present the exception message to the user. Use $exception->getMessage() to get it...
}
}
}
[b] Creating objects inside a class is a bad idea (like in ModUploadController). Use dependency injection instead. Read this and watch this and this. So the solution would look something like this:
class ModUploadController extends Mvc\Controller {
public function uploadMod(ModUploadModel $modUploadModel, ModModel $modModel): void {
//... Use the injected objects ($modUploadModel and $modModel ) ...
}
}
In a project, all objects that need to be injected into others can be created by a "dependency injection container". For example, PHP-DI (which I recommend), or other DI containers. So, a DI container takes care of all dependency injections of your project. For example, in your case, the two objects injected into ModUploadController::uploadMod method would be automatically created by PHP-DI. You'd just have to write three lines of codes in the file used as the entry-point of your app, probably index.php:
use DI\ContainerBuilder;
$containerBuilder = new ContainerBuilder();
$containerBuilder->useAutowiring(true);
$container = $containerBuilder->build();
Of course, a DI container requires configuration steps as well. But, in a couple of hours, you can understand how and where to do it.
By using a DI container, you'll be able to concentrate yourself solely on the logic of your project, not on how and where various components should be created, or similar tasks.
[c] Using static methods is a bad idea. My advise would be to get rid of all static methods that you already wrote. Watch this, read this, this and this. So the solution to the injection problem(s) that you have is the one above: the DI, perfomed by a DI container. Not at all creating static methods.
[d] You are using both components to query the database (ModModel with doesModNameExists() and ModUploadModel with insertIntoDb()). You should dedicate only one component to deal with the database.
[e] You don't need Mvc\Model at all.
[f] You don't need Mvc\Controller at all.
Some code:
I wrote some code, as an alternative to yours (from which I somehow "deduced" the tasks). Maybe it will help you, seeing how someone else would code. It would give you the possibility of "adding features to my mvc website without do any sort of spaghetti code". The code is very similar to the one from an answer that I wrote a short time ago. That answer also contains additional important suggestions and resources.
Important: Note that the application services, e.g. all components from Mvc/App/Service/, should communicate ONLY with the domain model components, e.g. with the components from Mvc/Domain/Model/ (mostly interfaces), not from Mvc/Domain/Infrastructure/. In turn, the DI container of your choice will take care of injecting the proper class implementations from Mvc/Domain/Infrastructure/ for the interfaces of Mvc/Domain/Model/ used by the application services.
Note: my code uses PHP 8.0. Good luck.
Project structure:
Mvc/App/Controller/Mod/AddMod.php:
<?php
namespace Mvc\App\Controller\Mod;
use Psr\Http\Message\{
ResponseInterface,
ServerRequestInterface,
};
use Mvc\App\Service\Mod\{
AddMod As AddModService,
Exception\ModAlreadyExists,
};
use Mvc\App\View\Mod\AddMod as AddModView;
class AddMod {
/**
* #param AddModView $addModView A view for presenting the response to the request back to the user.
* #param AddModService $addModService An application service for adding a mod to the model layer.
*/
public function __construct(
private AddModView $addModView,
private AddModService $addModService,
) {
}
/**
* Add a mod.
*
* The mod details are submitted from a form, using the HTTP method "POST".
*
* #param ServerRequestInterface $request A server request.
* #return ResponseInterface The response to the current request.
*/
public function addMod(ServerRequestInterface $request): ResponseInterface {
// Read the values submitted by the user.
$name = $request->getParsedBody()['name'];
$description = $request->getParsedBody()['description'];
// Add the mod.
try {
$mod = $this->addModService->addMod($name, $description);
$this->addModView->setMod($mod);
} catch (ModAlreadyExists $exception) {
$this->addModView->setErrorMessage(
$exception->getMessage()
);
}
// Present the results to the user.
$response = $this->addModView->addMod();
return $response;
}
}
Mvc/App/Service/Mod/Exception/ModAlreadyExists.php:
<?php
namespace Mvc\App\Service\Mod\Exception;
/**
* An exception thrown if a mod already exists.
*/
class ModAlreadyExists extends \OverflowException {
}
Mvc/App/Service/Mod/AddMod.php:
<?php
namespace Mvc\App\Service\Mod;
use Mvc\Domain\Model\Mod\{
Mod,
ModMapper,
};
use Mvc\App\Service\Mod\Exception\ModAlreadyExists;
/**
* An application service for adding a mod.
*/
class AddMod {
/**
* #param ModMapper $modMapper A data mapper for transfering mods
* to and from a persistence system.
*/
public function __construct(
private ModMapper $modMapper
) {
}
/**
* Add a mod.
*
* #param string|null $name A mod name.
* #param string|null $description A mod description.
* #return Mod The added mod.
*/
public function addMod(?string $name, ?string $description): Mod {
$mod = $this->createMod($name, $description);
return $this->storeMod($mod);
}
/**
* Create a mod.
*
* #param string|null $name A mod name.
* #param string|null $description A mod description.
* #return Mod The newly created mod.
*/
private function createMod(?string $name, ?string $description): Mod {
return new Mod($name, $description);
}
/**
* Store a mod.
*
* #param Mod $mod A mod.
* #return Mod The stored mod.
* #throws ModAlreadyExists The mod already exists.
*/
private function storeMod(Mod $mod): Mod {
if ($this->modMapper->modExists($mod)) {
throw new ModAlreadyExists(
'A mod with the name "' . $mod->getName() . '" already exists'
);
}
return $this->modMapper->saveMod($mod);
}
}
Mvc/App/View/Mod/AddMod.php:
<?php
namespace Mvc\App\View\Mod;
use Mvc\{
App\View\View,
Domain\Model\Mod\Mod,
};
use Psr\Http\Message\ResponseInterface;
/**
* A view for adding a mod.
*/
class AddMod extends View {
/** #var Mod A mod. */
private Mod $mod = null;
/**
* Add a mod.
*
* #return ResponseInterface The response to the current request.
*/
public function addMod(): ResponseInterface {
$bodyContent = $this->templateRenderer->render('#Templates/Mod/AddMod.html.twig', [
'activeNavItem' => 'AddMod',
'mod' => $this->mod,
'error' => $this->errorMessage,
]);
$response = $this->responseFactory->createResponse();
$response->getBody()->write($bodyContent);
return $response;
}
/**
* Set the mod.
*
* #param Mod $mod A mod.
* #return static
*/
public function setMod(Mod $mod): static {
$this->mod = $mod;
return $this;
}
}
Mvc/App/View/View.php:
<?php
namespace Mvc\App\View;
use Psr\Http\Message\ResponseFactoryInterface;
use SampleLib\Template\Renderer\TemplateRendererInterface;
/**
* A view.
*/
abstract class View {
/** #var string An error message */
protected string $errorMessage = '';
/**
* #param ResponseFactoryInterface $responseFactory A response factory.
* #param TemplateRendererInterface $templateRenderer A template renderer.
*/
public function __construct(
protected ResponseFactoryInterface $responseFactory,
protected TemplateRendererInterface $templateRenderer
) {
}
/**
* Set the error message.
*
* #param string $errorMessage An error message.
* #return static
*/
public function setErrorMessage(string $errorMessage): static {
$this->errorMessage = $errorMessage;
return $this;
}
}
Mvc/Domain/Infrastructure/Mod/PdoModMapper.php:
<?php
namespace Mvc\Domain\Infrastructure\Mod;
use Mvc\Domain\Model\Mod\{
Mod,
ModMapper,
};
use PDO;
/**
* A data mapper for transfering Mod entities to and from a database.
*
* This class uses a PDO instance as database connection.
*/
class PdoModMapper implements ModMapper {
/**
* #param PDO $connection Database connection.
*/
public function __construct(
private PDO $connection
) {
}
/**
* #inheritDoc
*/
public function modExists(Mod $mod): bool {
$sql = 'SELECT COUNT(*) as cnt FROM mods WHERE name = :name';
$statement = $this->connection->prepare($sql);
$statement->execute([
':name' => $mod->getName(),
]);
$data = $statement->fetch(PDO::FETCH_ASSOC);
return ($data['cnt'] > 0) ? true : false;
}
/**
* #inheritDoc
*/
public function saveMod(Mod $mod): Mod {
if (isset($mod->getId())) {
return $this->updateMod($mod);
}
return $this->insertMod($mod);
}
/**
* Update a mod.
*
* #param Mod $mod A mod.
* #return Mod The mod.
*/
private function updateMod(Mod $mod): Mod {
$sql = 'UPDATE mods
SET
name = :name,
description = :description
WHERE
id = :id';
$statement = $this->connection->prepare($sql);
$statement->execute([
':name' => $mod->getName(),
':description' => $mod->getDescription(),
]);
return $mod;
}
/**
* Insert a mod.
*
* #param Mod $mod A mod.
* #return Mod The newly inserted mod.
*/
private function insertMod(Mod $mod): Mod {
$sql = 'INSERT INTO mods (
name,
description
) VALUES (
:name,
:description
)';
$statement = $this->connection->prepare($sql);
$statement->execute([
':name' => $mod->getName(),
':description' => $mod->getDescription(),
]);
$mod->setId(
$this->connection->lastInsertId()
);
return $mod;
}
}
Mvc/Domain/Model/Mod/Mod.php:
<?php
namespace Mvc\Domain\Model\Mod;
/**
* Mod entity.
*/
class Mod {
/**
* #param string|null $name (optional) A name.
* #param string|null $description (optional) A description.
*/
public function __construct(
private ?string $name = null,
private ?string $description = null
) {
}
/**
* Get id.
*
* #return int|null
*/
public function getId(): ?int {
return $this->id;
}
/**
* Set id.
*
* #param int|null $id An id.
* #return static
*/
public function setId(?int $id): static {
$this->id = $id;
return $this;
}
/**
* Get the name.
*
* #return string|null
*/
public function getName(): ?string {
return $this->name;
}
/**
* Set the name.
*
* #param string|null $name A name.
* #return static
*/
public function setName(?string $name): static {
$this->name = $name;
return $this;
}
/**
* Get the description.
*
* #return string|null
*/
public function getDescription(): ?string {
return $this->description;
}
/**
* Set the description.
*
* #param string|null $description A description.
* #return static
*/
public function setDescription(?string $description): static {
$this->description = $description;
return $this;
}
}
Mvc/Domain/Model/Mod/ModMapper.php:
<?php
namespace Mvc\Domain\Model\Mod;
use Mvc\Domain\Model\Mod\Mod;
/**
* An interface for various data mappers used to
* transfer Mod entities to and from a persistence system.
*/
interface ModMapper {
/**
* Check if a mod exists.
*
* #param Mod $mod A mod.
* #return bool True if the mod exists, false otherwise.
*/
public function modExists(Mod $mod): bool;
/**
* Save a mod.
*
* #param Mod $mod A mod.
* #return Mod The saved mod.
*/
public function saveMod(Mod $mod): Mod;
}
I am using Nicolas Widart's Laravel Modules package to help manage a large app, and keep everything separated into logical modules. I would like to be able to drop in different modules and have them play nicely without any extra configuration.
All of my modules will define interfaces and default implementations that allow the application (the system controlling which modules are loaded) to specify that it wants to use a specific implementation instead, through dependency injection.
I am able to make some assumptions by having some modules require others, for example a payment processing module (Module PP) can assume that a payment is tied to a user (with which the interface for a user is defined in another module, Module U).
My ideal scenario is that I could add to an existing PHP interface that is defined in another required module. For example, being able to retrieve a user from a repository defined in Module U and call a method on it that was defined in Module PP.
Once Module PP resolves the interface (again, through dependency injection) from Module U to a class, I want my method from Module PP to be callable on that class.
I have been able to achieve this using the __call magic method as below.
Extensions Module
This module defines the core operations to add to an existing interface.
IsExtendable Interface
<?php
namespace Modules\Extensions\Contracts;
interface IsExtendable
{
/**
* Get the list of extensions for this entity.
*
* #return array
*/
public static function getExtensions();
/**
* Adds an extension to this entity.
*
* #param string $name
* #param mixed $function
*/
public static function addExtension($name, $function);
/**
* Checks whether the entity has the given extension.
*
* #param string $name
*
* #return bool
*/
public static function hasExtension($name);
/**
* Call the extension if it exists, or pass it further up the chain.
*
* #param string $name
* #param mixed $arguments
*
* #return mixed
*/
public function __call($name, $arguments);
}
IsExtendable Trait
<?php
namespace Modules\Extensions;
trait IsExtendable
{
/** #var $extensions */
private static $extensions = [];
/**
* Get the list of extensions for this entity.
*
* #return array
*/
public static function getExtensions()
{
return self::$extensions;
}
/**
* Adds an extension to this entity.
*
* #param string $name
* #param mixed $function
*/
public static function addExtension($name, $function)
{
if(is_callable($function) == FALSE)
{
throw new \InvalidArgumentException('Function must be callable.');
}
self::$extensions[$name] = $function;
}
/**
* Checks whether the entity has the given extension.
*
* #param string $name
*
* #return bool
*/
public static function hasExtension($name)
{
return array_key_exists($name, self::getExtensions()) == TRUE;
}
/**
* Calls the extension if it exists, or passes it further up the chain.
*
* #param string $name
* #param mixed $arguments
*
* #return mixed
*/
public function __call($name, $arguments)
{
if(self::hasExtension($name) == TRUE)
{
$callable = self::getExtensions()[$name];
return call_user_func_array($callable, array_merge(array($this), $arguments));
}
else
{
return parent::__call($name, $arguments);
}
}
}
Service Provider
<?php
namespace Modules\Extensions\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\Extensions\Contracts\IsExtendable as IsExtendableContract;
class ExtensionServiceProvider extends ServiceProvider
{
/**
* #param string $implementation
* #param string $functionName
*
* #return callable
*/
public function prepareExtension($implementation, $functionName)
{
return $implementation . '::' . $functionName;
}
/**
* #param string $contract
* #param string $implementation
*
* #return void
*/
public function extractExtensions($contract, $implementation)
{
$reflection = new \ReflectionClass($implementation);
$methods = [];
foreach($reflection->getMethods(\ReflectionMethod::IS_STATIC) as $method)
{
// TODO: May be able to use $method->getClosure() here
// https://stackoverflow.com/questions/8299886/php-get-static-methods
$methods[] = $method->getName();
}
$this->registerExtensions($contract, $methods, $implementation);
}
/**
* #param string $contract
* #param string $name
* #param string $function
*
* #return void
*/
public function registerExtension($contract, $name, $function)
{
// Resolve the contract to an implementation
$base = app($contract);
// Check that it is suitable for extension
if($base instanceof IsExtendableContract)
{
$base::addExtension($name, $function);
}
}
/**
* #param string $contract
* #param array $extensions
* #param string|null $implementation
*
* #return void
*/
public function registerExtensions($contract, array $extensions = [], $implementation = NULL)
{
// Resolve the contract to an implementation
$base = app($contract);
// Check that it is suitable for extension
if($base instanceof IsExtendableContract)
{
foreach($extensions as $name => $function)
{
if(is_int($name) == TRUE)
{
if(is_string($function) == TRUE)
{
$name = $function;
}
else
{
throw new \InvalidArgumentException('All extensions must have a valid name.');
}
}
if(is_string($function) == TRUE)
{
if(strpos($function, '::') === FALSE && $implementation != NULL)
{
$function = $this->prepareExtension($implementation, $function);
}
}
$base::addExtension($name, $function);
}
}
}
}
Module U
User Interface
<?php
namespace Modules\Auth\Contracts\Entities;
interface User
{
/**
* #return int
*/
public function getId();
/**
* #return string
*/
public function getName();
/**
* #return string
*/
public function getEmail();
/**
* #return \DateTime
*/
public function getCreatedAt();
/**
* #return \DateTime
*/
public function getUpdatedAt();
}
User Implementation
<?php
namespace Modules\Auth\Entities;
use Modules\Extensions\Contracts\IsExtendable as IsExtendableContract;
use Modules\Auth\Contracts\Entities\User as UserContract;
use Modules\Extensions\IsExtendable;
class User implements
IsExtendableContract,
UserContract
{
use IsExtendable;
/**
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* #return string
*/
public function getEmail()
{
return $this->email;
}
/**
* #return \DateTime
*/
public function getCreatedAt()
{
return $this->created_at;
}
/**
* #return \DateTime
*/
public function getUpdatedAt()
{
return $this->updated_at;
}
}
Module PP
User Extension
<?php
namespace Modules\Test\Entities\Extensions;
use Modules\Auth\Contracts\Entities\User;
class UserExtension
{
/**
* #param User $context
*/
public static function getCardLastFour($context)
{
return $context->card_last_four;
}
/**
* #param User $context
*/
public static function getCardBrand($context)
{
return $context->card_brand;
}
/**
* #param User $context
*/
public static function getStripeId($context)
{
return $context->stripe_id;
}
}
Service Provider
<?php
namespace Modules\Test\Providers\Extensions;
use Modules\Auth\Contracts\Entities\User as UserContract;
use Modules\Test\Entities\Extensions\UserExtension;
use Modules\Extensions\Providers\ExtensionServiceProvider;
class StripeExtensionProvider extends ExtensionServiceProvider
{
public function boot()
{
// TODO: Set the contract as a static field on the extension to then automatically extract from all extension files in a folder
$this->extractExtensions(UserContract::class, UserExtension::class);
}
}
My question is, is this method scalable (across maybe 10 modules), and can you foresee any issues with it? Or is there a better/more popular (and supported) way to do this? I don't want to get 2 years into a project and discover that I really hate the way I've implemented this.
I know that this concept won't support IDE autocompletion out of the box but I could build in a way to generate the PHPDocs similar to this package.
I have researched the Decorator pattern but this feels clunky in that I would always need to rely on a new implementation within each module, instead of just adding to the existing one.
I realise this is a big question so my sincere thanks to anyone willing to have a look at it!
Check out Laravel's macroable trait. It's basically the same idea, and Laravel uses it all over the place.
So yes, it scales - up to a certain point. Like almost everything else, this is a tool that can be abused. Use it with a dash of common sense, and you should be OK.
I have the following class
namespace PG\Referrer\Single\Post;
class Referrer implements ReferrerInterface
{
/**
* #var $authorReferrer = null
*/
protected $isAuthorReferrer = null;
/**
* #var $dateReferrer = null
*/
protected $isDateReferrer = null;
/**
* #var $searchReferrer = null
*/
protected $isSearchReferrer = null;
/**
* #var $taxReferrer = null
*/
protected $isTaxReferrer = null;
/**
* #param array $values = null;
*/
public function __construct(array $values = null)
{
if ($values)
$this->setBulk($values);
}
/**
* Bulk setter Let you set the variables via array or object
*/
public function setBulk($values)
{
if (!is_array($values) && !$values instanceof \stdClass) {
throw new \InvalidArgumentException(
sprintf(
'%s needs either an array, or an instance of \\stdClass to be passed, instead saw %s',
__METHOD__,
is_object($values) ? get_class($values) : gettype($values)
)
);
}
foreach ($values as $name => $value) {//create setter from $name
global $wp_query;
if (array_key_exists($value, $wp_query->query_vars)) { //Check that user don't set a reserved query vars
throw new \InvalidArgumentException(
sprintf(
'%s is a reserved query_vars and cannot be used. Please use a unique value',
$value
)
);
}
$setter = 'set' . $name;
$condition = isset($_GET[$value]);
if ($setter !== 'setBulk' && method_exists($this, $setter)) {
$this->{$setter}($condition);//set value (bool)
}
}
return $this;
}
/**
* #param bool $authorReferrer
* #return $this
*/
public function setAuthorReferrer($isAuthorReferrer)
{
$this->isAuthorReferrer = $isAuthorReferrer;
return $this;
}
/**
* #param bool $dateReferrer
* #return $this
*/
public function setDateReferrer($isDateReferrer)
{
$this->isDateReferrer = $isDateReferrer;
return $this;
}
/**
* #param bool $searchReferrer
* #return $this
*/
public function isSearchReferrer($isSearchReferrer)
{
$this->isSearchReferrer = $isSearchReferrer;
return $this;
}
/**
* #param bool $taxReferrer
* #return $this
*/
public function setTaxReferrer($isTaxReferrer)
{
$this->isTaxReferrer = $isTaxReferrer;
return $this;
}
}
with its interface
namespace PG\Referrer\Single\Post;
interface ReferrerInterface
{
/**
* #param array $values
* #return $this
*/
public function setBulk($values);
/**
* #param bool $authorReferrer
* #return $this
*/
public function setAuthorReferrer($isAuthorReferrer);
/**
* #param bool $dateReferrer
* #return $this
*/
public function setDateReferrer($isDateReferrer);
/**
* #param bool $searchReferrer
* #return $this
*/
public function isSearchReferrer($isSearchReferrer);
/**
* #param bool $taxReferrer
* #return $this
*/
public function setTaxReferrer($isTaxReferrer);
}
This class sets up 4 conditionals that I need to use in another class. The values that is used in this class is also set from the other class, so basically the user sets values in the other class (lets call it class b) that is then used by class Referrer and returns the 4 conditionals which is then used by class b.
The reason why I'm doing it this way is because there will be two other classes that will need to do the same, but will returns different info
What is the more correct way to achieve this?
EDIT
To clear this up
class Referrer
The properties $isAuthorReferrer, $isDateReferreretc will either have a value of null or a boolean value depending on what is set by the user.
Example:
$q = new Referrer(['authorReferrer' => 'aq']);
In the code above, $isAuthorReferrer is set via the setBulk() method in the class to true when the variable aq is available in the URL or false when not present. The three other properties will return null because they are not set in the example.
The above works as expected, but I need to do this in another class, lets again call it class b. The arguments will be set to class b, and in turn, class b will set this arguments to class Referrer, class Referrer will use this arguments and return the proper values of its properties, and class b will use this results to do something else
Example:
$q = new b(['authorReferrer' => 'aq']);
Where class b could be something like this (it is this part that I'm not sure how to code)
class b implements bInterface
{
protected $w;
protected $other;
public function __construct($args = [])
{
//Do something here
// Do something here so that we can use $other in other classes or functions
}
public function a()
{
$w = new Referrer($args);
}
public function b()
{
// use $w properties here
// return $other for usage in other classes and functions
}
}
The best way is to inject the referrer to your classes in order to do loose coupling between them and the referrer (this pattern use the benefit of your ReferrerInterface):
class b implements bInterface
{
protected $referrer;
public function __construct(ReferrerInterface $referrer, array $values = array())
{
$this->referrer = $referrer;
$this->referrer->setBulk($values);
}
public function getReferrer()
{
return $this->referrer;
}
public function b()
{
// use $this->referrer properties here
}
}
// Instantiation (use your dependency injection if you have one):
$referrer = new Referrer();
$b = new b($referrer, ['authorReferrer' => 'aq']);
I do not understand what is $other so I removed it but explain me if you want me to I add it again.
If you need to use the properties of the referrer in b, you should add some getters in your ReferrerInterface to allow that. I would use setAuthorReferrer($isAuthorReferrer) to set the value and isAuthorReferrer() to get it for instance.
In my php application I have been comparing objects with the usual equality comparison operator, e.g.:
if ($objectA == $objectB) { ... }
Recently I implemented proxies (for objects which are expensive to load) however this means the equality operator no longer works. Is there a simple way around this? One that doesn't rely on reflection?
For the moment, I have resorted to testing the unique identifier of each object, e.g.
if ($objectA->getId() == $objectB->getId) { ... }
But this has two problems: 1) I need to refactor all existing code, and 2) in the future I may need to compare objects which are value objects (not entities).
I'm not hopeful of an easy solution since I think it would require a new magic method...
Here's my AbstractProxy class. Any help appreciated...
abstract class KOOP_Base_AbstractProxy
implements KOOP_Base_iDomain
{
use KOOP_Trait_Helper_Helper;
/**
* #var integer Object identifier
*/
protected $_id = null;
/**
* #var KOOP_Base_AbstractMapper
*/
protected $_mapper = null;
/**
* #var KOOP_Base_AbstractDomain Actual object
*/
protected $_subject = null;
/**
* Store object id for lazy loading
*
* #param integer $id Object identifier
* #param string $mapper Mapper by which to retrieve object
*/
public function __construct($id, $mapper)
{
$this->_id = $id;
$this->_mapper = $mapper;
}
/**
* Get subject
*
* #return KOOP_Base_AbstractDomain
*/
protected function getSubject()
{
if (!$this->_subject) {
$this->_subject = $this->getMapper($this->_mapper)->find($this->_id);
}
return $this->_subject;
}
/**
* Get property
*
* #param string $property
* #return mixed
*/
public function __get($property)
{
return $this->getSubject()->$property;
}
/**
* Set property
*
* #param string $property
* #param mixed $value
* #return void
*/
public function __set($property, $value)
{
$this->getSubject()->$property = $value;
}
/**
* Is property set?
*
* #param $property
* #return boolean
*/
public function __isset($property)
{
return isset($this->getSubject()->$property);
}
/**
* Unset property
*
* #param string $property
* #return mixed
*/
public function __unset($property)
{
unset($this->getSubject()->$property);
}
/**
* Call method
*
* #param string $method Method to call
* #param array $params Parameters to pass
* #return mixed
*/
public function __call($method, array $params)
{
return call_user_func_array(array($this->getSubject(), $method), $params);
}
/**
* Get id
*
* Saves having to retrieve the entire object when only the ID is required.
*/
public function getId()
{
return $this->_id;
}
}
Proxies do break object equality, and there's no utterly clean way to fix this. In a fully object oriented language you would handle this by operator overloading (which I don't recommend) or implementing a custom .equals() function (as in Java). Sadly, PHP simply does not support object orientation at this level, so you will have some decisions to make.
1) I would prefer to have your proxy class provide an equals() function which takes as input a reference to the object you want to test against and compares it to the proxied object - which shouldn't be much more 'expensive' than it was to not use a proxy at all. Example in pseudo-PHP code (my apologies if my reference syntax is off, it's been a while):
public function equals (&$toCompare)
{
if ($_subject == $toCompare)
{
return true;
}
else
{
return false;
}
}
The downside is simple: you have to refactor your code that involves this proxied object, and you have to remember that "==" does not work on this proxied object type while you are working. If you don't deal with these objects much, or if you deal with them all the time, this is fine. If you deal with them regularly but intermittently, or if others must work with them on occasion, then this will cause bugs when you/they forget about this equality problem.
2) Use an Operator Overloading extension to the language. I haven't done this, I don't know if it works, and it might be a nightmare. I include it for theoretical completeness.
Personally, I think I'd just hack it with the pseudo-Java approach call it a day, as I think it would actually work and require nothing more than using the function correctly (and remembering to use it in the first place).
I am right now getting myself more and more familiar with Zend Framework 2 and in the meantime I was getting myself updated with the validation part in Zend Framework 2. I have seen few examples how to validate the data from the database using Zend Db adapter, for example the code from the Zend Framework 2 official website:
//Check that the username is not present in the database
$validator = new Zend\Validator\Db\NoRecordExists(
array(
'table' => 'users',
'field' => 'username'
)
);
if ($validator->isValid($username)) {
// username appears to be valid
} else {
// username is invalid; print the reason
$messages = $validator->getMessages();
foreach ($messages as $message) {
echo "$message\n";
}
}
Now my question is how can do the validation part?
For example, I need to validate a name before inserting into database to check that the same name does not exist in the database, I have updated Zend Framework 2 example Album module to use Doctrine 2 to communicate with the database and right now I want to add the validation part to my code.
Let us say that before adding the album name to the database I want to validate that the same album name does not exist in the database.
Any information regarding this would be really helpful!
if you use the DoctrineModule, there is already a validator for your case.
I had the same problem and solved it this way:
Create a custom validator class, name it something like NoEntityExists (or whatever you want).
Extend Zend\Validator\AbstractValidator
Provide a getter and setter for Doctrine\ORM\EntityManager
Provide some extra getters and setters for options (entityname, ...)
Create an isValid($value) method that checks if a record exists and returns a boolean
To use it, create a new instance of it, assign the EntityManager and use it just like any other validator.
To get an idea of how to implement the validator class, check the validators that already exist (preferably a simple one like Callback or GreaterThan).
Hope I could help you.
// Edit: Sorry, I'm late ;-)
So here is a quite advanced example of how you can implement such a validator.
Note that I added a translate() method in order to catch language strings with PoEdit (a translation helper tool that fetches such strings from the source codes and puts them into a list for you). If you're not using gettext(), you can problably skip that.
Also, this was one of my first classes with ZF2, I wouldn't put this into the Application module again. Maybe, create a new module that fits better, for instance MyDoctrineValidator or so.
This validator gives you a lot of flexibility as you have to set the query before using it. Of course, you can pre-define a query and set the entity, search column etc. in the options. Have fun!
<?php
namespace Application\Validator\Doctrine;
use Zend\Validator\AbstractValidator;
use Doctrine\ORM\EntityManager;
class NoEntityExists extends AbstractValidator
{
const ENTITY_FOUND = 'entityFound';
protected $messageTemplates = array();
/**
* #var EntityManager
*/
protected $entityManager;
/**
* #param string
*/
protected $query;
/**
* Determines if empty values (null, empty string) will <b>NOT</b> be included in the check.
* Defaults to true
* #var bool
*/
protected $ignoreEmpty = true;
/**
* Dummy to catch messages with PoEdit...
* #param string $msg
* #return string
*/
public function translate($msg)
{
return $msg;
}
/**
* #return the $ignoreEmpty
*/
public function getIgnoreEmpty()
{
return $this->ignoreEmpty;
}
/**
* #param boolean $ignoreEmpty
*/
public function setIgnoreEmpty($ignoreEmpty)
{
$this->ignoreEmpty = $ignoreEmpty;
return $this;
}
/**
*
* #param unknown_type $entityManager
* #param unknown_type $query
*/
public function __construct($entityManager = null, $query = null, $options = null)
{
if(null !== $entityManager)
$this->setEntityManager($entityManager);
if(null !== $query)
$this->setQuery($query);
// Init messages
$this->messageTemplates[self::ENTITY_FOUND] = $this->translate('There is already an entity with this value.');
return parent::__construct($options);
}
/**
*
* #param EntityManager $entityManager
* #return \Application\Validator\Doctrine\NoEntityExists
*/
public function setEntityManager(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
return $this;
}
/**
* #return the $query
*/
public function getQuery()
{
return $this->query;
}
/**
* #param field_type $query
*/
public function setQuery($query)
{
$this->query = $query;
return $this;
}
/**
* #return \Doctrine\ORM\EntityManager
*/
public function getEntityManager()
{
return $this->entityManager;
}
/**
* (non-PHPdoc)
* #see \Zend\Validator\ValidatorInterface::isValid()
* #throws Exception\RuntimeException() in case EntityManager or query is missing
*/
public function isValid($value)
{
// Fetch entityManager
$em = $this->getEntityManager();
if(null === $em)
throw new Exception\RuntimeException(__METHOD__ . ' There is no entityManager set.');
// Fetch query
$query = $this->getQuery();
if(null === $query)
throw new Exception\RuntimeException(__METHOD__ . ' There is no query set.');
// Ignore empty values?
if((null === $value || '' === $value) && $this->getIgnoreEmpty())
return true;
$queryObj = $em->createQuery($query)->setMaxResults(1);
$entitiesFound = !! count($queryObj->execute(array(':value' => $value)));
// Set Error message
if($entitiesFound)
$this->error(self::ENTITY_FOUND);
// Valid if no records are found -> result count is 0
return ! $entitiesFound;
}
}