Laravel caching authenticated user's relationships - php

In my application I use Laravel's authentication system and I use dependency injection (or the Facade) to access the logged in user. I tend to make the logged in user accessible through my base controller so I can access it easily in my child classes:
class Controller extends BaseController
{
protected $user;
public function __construct()
{
$this->user = \Auth::user();
}
}
My user has a number of different relationships, that I tend to eager load like this:
$this->user->load(['relationshipOne', 'relationshipTwo']);
As in this project I'm expecting to receive consistently high volumes of traffic, I want to make the application run as smoothly and efficiently as possible so I am looking to implement some caching.
I ideally, need to be able to avoid repeatedly querying the database, particularly for the user's related records. As such I need to look into caching the user object, after loading relationships.
I had the idea to do something like this:
public function __construct()
{
$userId = \Auth::id();
if (!is_null($userId)) {
$this->user = \Cache::remember("user-{$userId}", 60, function() use($userId) {
return User::with(['relationshipOne', 'relationshipTwo'])->find($userId);
});
}
}
However, I'm unsure whether or not it's safe to rely on whether or not \Auth::id() returning a non-null value to pass authentication. Has anyone faced any similar issues?

I would suggest you used a package like the following one. https://github.com/spatie/laravel-responsecache
It caches the response and you can use it for more than just the user object.

Well, after some messing about I've come up with kind of a solution for myself which I thought I would share.
I thought I would give up on caching the actual User object, and just let the authentication happen as normal and just focus on trying to cache the user's relations. This feels like quite a dirty way to do it, since my logic is in the model:
class User extends Model
{
// ..
/**
* This is the relationship I want to cache
*/
public function related()
{
return $this->hasMany(Related::class);
}
/**
* This method can be used when we want to utilise a cache
*/
public function getRelated()
{
return \Cache::remember("relatedByUser({$this->id})", 60, function() {
return $this->related;
});
}
/**
* Do something with the cached relationship
*/
public function totalRelated()
{
return $this->getRelated()->count();
}
}
In my case, I needed to be able to cache the related items inside the User model because I had some methods inside the user that would use that relationship. Like in the pretty trivial example of the totalRelated method above (My project is a bit more complex).
Of course, if I didn't have internal methods like that on my User model it would have been just as easy to call the relationship from outside my model and cache that (In a controller for example)
class MyController extends Controller
{
public function index()
{
$related = \Cache::remember("relatedByUser({$this->user->id})", 60, function() {
return $this->user->related;
});
// Do something with the $related items...
}
}
Again, this doesn't feel like the best solution to me and I am open to try other suggestions.
Cheers
Edit: I've went a step further and implemented a couple of methods on my parent Model class to help with caching relationships and implemented getter methods for all my relatonships that accept a $useCache parameter, to make things a bit more flexible:
Parent Model class:
class Model extends BaseModel
{
/**
* Helper method to get a value from the cache if it exists, or using the provided closure, caching the result for
* the default cache time.
*
* #param $key
* #param Closure|null $callback
* #return mixed
*/
protected function cacheRemember($key, Closure $callback = null)
{
return Cache::remember($key, Cache::getDefaultCacheTime(), $callback);
}
/**
* Another helper method to either run a closure to get a value, or if useCache is true, attempt to get the value
* from the cache, using the provided key and the closure as a means of getting the value if it doesn't exist.
*
* #param $useCache
* #param $key
* #param Closure $callback
* #return mixed
*/
protected function getOrCacheRemember($useCache, $key, Closure $callback)
{
return !$useCache ? $callback() : $this->cacheRemember($key, $callback);
}
}
My User class:
class User extends Model
{
public function related()
{
return $this->hasMany(Related::class);
}
public function getRelated($useCache = false)
{
return $this->getOrCacheRemember($useCache, "relatedByUser({$this->id})", function() {
return $this->related;
});
}
}
Usage:
$related = $user->getRelated(); // Gets related from the database
$relatedTwo = $user->getRelated(true); // Gets related from the cache if present (Or from database and caches result)

Related

How to construct an object that depends on the session to be loaded

Let's suppose we have a site that shows a random list of 20 movies. Logged in users, however, can select their favorite movies, so those movies will be shown instead. This list of movies is shown both in the home page and in some other pages.
To follow the DRY principle, we could encapsulate this logic in its own class, and then inject this class wherever it is necessary to show the list of movies. This class will also have other methods that will be used throughout the application. For example, there is also a method to get one random movie.
The class could look like this (please note this is a simplified example):
class MovieService
{
/** #var Collection $movies */
protected $movies;
public function __construct()
{
$this->movies = Auth::check() ? Auth::user()->favoriteMovies : $this->randomMovies();
}
public function getRandomMovies(): Collection
{
return $this->movies->random(20);
}
public function getOneRandom(): Movie {
return $this->movies->random();
}
protected function randomMovies() {
return Movie::inRandomOrder()->take(20)->get();
}
}
Note: Please note that this is an example and that some things could be improved.
As this class could be used multiple times in the same request, it is a good idea to make it a singleton in the IoC container, so that the queries that are run when instantiated are not run more than once.
However, now we encounter a problem. We need this class in a private method in a controller. We could directly call the app container like app() or App::make() but we would like to avoid facades and global helpers with custom dependencies.
class HomeController extends Controller
{
/** #var MovieService $movieService */
protected $movieService;
public function __construct(MovieService $movieService)
{
$this->movieService = $movieService;
}
public function index()
{
$movies = $this->getMovies();
return view('home', compact('movies'));
}
protected function getMovies()
{
// Let's imagine there's some extra logic here so that we would actually need this method.
return $this->movieService->getRandomMovies();
}
}
We have found a problem. A controller's constructor is run before the middleware pipeline, which means that there's no session and, hence, no user identification. Now Auth::check() in MovieService is always returning false, so the default movies will always be shown.
What would you do to fix this?
It's cleaner to not use the constructor of an object for logic, only for managing dependencies. Coincidentally this will also fix the issue you're having by moving the Auth::check() logic to your getter methods instead. Besides that you could also consider injecting the AuthManager instead of relying on the Auth facade, but that's just a sidenote.
class MovieService
{
/** #var AuthManager $auth */
protected $auth;
protected $movies;
public function __construct(Illuminate\Auth\AuthManager $auth)
{
$this->auth = $auth;
}
public function getRandomMovies(): Collection
{
return $this->getMoviesForCurrentUser()->random(20);
}
public function getOneRandom(): Movie {
return $this->getMoviesForCurrentUser()->random();
}
protected function randomMovies() {
if ($this->movies === null) {
$this->movies = Movie::inRandomOrder()->take(20)->get();
}
return $this->movies;
}
protected function getMoviesForCurrentUser() {
if ($this->auth->check()) {
return $this->auth->user->favoriteMovies;
}
return $this->randomMovies();
}
}

Laravel 5: Type-hinting a FormRequest class inside a controller that extends from BaseController

I have a BaseController that provides the foundation for most HTTP methods for my API server, e.g. the store method:
BaseController.php
/**
* Store a newly created resource in storage.
*
* #return Response
*/
public function store(Request $request)
{
$result = $this->repo->create($request);
return response()->json($result, 200);
}
I then extend on this BaseController in a more specific controller, such as the UserController, like so:
UserController.php
class UserController extends BaseController {
public function __construct(UserRepository $repo)
{
$this->repo = $repo;
}
}
This works great. However, I now want to extend UserController to inject Laravel 5's new FormRequest class, which takes care of things like validation and authentication for the User resource. I would like to do this like so, by overwriting the store method and using Laravel's type hint dependency injection for its Form Request class.
UserController.php
public function store(UserFormRequest $request)
{
return parent::store($request);
}
Where the UserFormRequest extends from Request, which itself extends from FormRequest:
UserFormRequest.php
class UserFormRequest extends Request {
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'name' => 'required',
'email' => 'required'
];
}
}
The problem is that the BaseController requires a Illuminate\Http\Request object whereas I pass a UserFormRequest object. Therefore I get this error:
in UserController.php line 6
at HandleExceptions->handleError('2048', 'Declaration of Bloomon\Bloomapi3\Repositories\User\UserController::store() should be compatible with Bloomon\Bloomapi3\Http\Controllers\BaseController::store(Illuminate\Http\Request $request)', '/home/tom/projects/bloomon/bloomapi3/app/Repositories/User/UserController.php', '6', array('file' => '/home/tom/projects/bloomon/bloomapi3/app/Repositories/User/UserController.php')) in UserController.php line 6
So, how can I type hint inject the UserFormRequest while still adhering to the BaseController's Request requirement? I cannot force the BaseController to require a UserFormRequest, because it should work for any resource.
I could use an interface like RepositoryFormRequest in both the BaseController and the UserController, but then the problem is that Laravel no longer injects the UserFormController through its type hinting dependency injection.
In contrast to many 'real' object oriented languages, this kind of type hinting design in overridden methods is just not possible in PHP, see:
class X {}
class Y extends X {}
class A {
function a(X $x) {}
}
class B extends A {
function a(Y $y) {} // error! Methods with the same name must be compatible with the parent method, this includes the typehints
}
This produces the same kind of error as your code. I would just not put a store() method in your BaseController. If you feel that you are repeating code, consider introducing for example a service class or maybe a trait.
Using a service class
Below a solution that makes use of an extra service class. This might be overkill for your situation. But if you add more functionality to the StoringServices store() method (like validation), it could be useful. You can also add more methods to the StoringService like destroy(), update(), create(), but then you probably want to name the service differently.
class StoringService {
private $repo;
public function __construct(Repository $repo)
{
$this->repo = $repo;
}
/**
* Store a newly created resource in storage.
*
* #return Response
*/
public function store(Request $request)
{
$result = $this->repo->create($request);
return response()->json($result, 200);
}
}
class UserController {
// ... other code (including member variable $repo)
public function store(UserRequest $request)
{
$service = new StoringService($this->repo); // Or put this in your BaseController's constructor and make $service a member variable
return $service->store($request);
}
}
Using a trait
You can also use a trait, but you have to rename the trait's store() method then:
trait StoringTrait {
/**
* Store a newly created resource in storage.
*
* #return Response
*/
public function store(Request $request)
{
$result = $this->repo->create($request);
return response()->json($result, 200);
}
}
class UserController {
use {
StoringTrait::store as baseStore;
}
// ... other code (including member variable $repo)
public function store(UserRequest $request)
{
return $this->baseStore($request);
}
}
The advantage of this solution is that if you do not have to add extra functionality to the store() method, you can just use the trait without renaming and you do not have to write an extra store() method.
Using inheritance
In my opinion, inheritance is not so suitable for the kind of code reuse that you need here, at least not in PHP. But if you want to only use inheritance for this code reuse problem, give the store() method in your BaseController another name, make sure that all classes have their own store() method and call the method in the BaseController. Something like this:
BaseController.php
/**
* Store a newly created resource in storage.
*
* #return Response
*/
protected function createResource(Request $request)
{
$result = $this->repo->create($request);
return response()->json($result, 200);
}
UserController.php
public function store(UserFormRequest $request)
{
return $this->createResource($request);
}
You can move your logic from BaseController to trait, service, facade.
You can not override existing function and force it to use different type of argument, it would break stuff. For example, if you later would write this:
function foo(BaseController $baseController, Request $request) {
$baseController->store($request);
}
It would break with your UserController and OtherRequest because UserController expects UserController, not OtherRequest (which extends Request and is valid argument from foo() perspective).
As others have mentioned, you cannot do what you want to do for a host of reasons. As mentioned, you can solve this problem with traits or similar. I am presenting an alternative approach.
At a guess, it sounds like you are trying to follow the naming convention put forth by Laravel's RESTful Resource Controllers, which is kind of forcing you to use a particular method on a controller, in this case, store.
Looking at the source of ResourceRegistrar.php we can see that in the getResourceMethods method, Laravel does either a diff or intersect with the options array you pass in and against the default values. However, the those defaults are protected, and include store.
What this means is that you can't pass anything to Route::resource to force some override of the route names. So let's rule that out.
A simpler approach would be to simply set up a different method just for this route. This can be achieved by doing:
Route::post('user/save', 'UserController#save');
Route::resource('users', 'UserController');
Note: As per the documentation, the custom routes must come prior to the Route::resource call.
The declaration of UserController::store() should be compatible with BaseController::store(), which means (among other things) that the given parameters for both the BaseController as well as UserController should be exactly the same.
You actually cán force the BaseController to require a UserFormRequest, it's not the prettiest solution, but it works.
By overwriting there is no way you can replace Request with UserFormRequest, so why not use both? Giving both methods an optional parameter for injecting the UserFormRequest object. Which would result in:
BaseController.php
class BaseController {
public function store(Request $request, UserFormRequest $userFormRequest = null)
{
$result = $this->repo->create($request);
return response()->json($result, 200);
}
}
UserController.php
class UserController extends BaseController {
public function __construct(UserRepository $repo)
{
$this->repo = $repo;
}
public function store(UserFormRequest $request, UserFormRequest $userFormRequest = null)
{
return parent::store($request);
}
}
This way you can ignore the parameter when using BaseController::store() and inject it when using UserController::store().
The easiest and cleanest way I found to circumvent that problem was to prefix the parent methods with an underscore. For example:
BaseController:
_store(Request $request) { ... }
_update(Request $request) { ... }
UserController:
store(UserFormRequest $request) { return parent::_store($request); }
update(UserFormRequest $request) { return parent::_update($request); }
I feel like creating service providers is an overkill. What we're trying to circumvent here is not the Liskov substitution principle, but simply the lack of proper PHP reflection. Type-hinting methods is, in itself, a hack after all.
This will force you to manually implement a store and update in every child controller. I don't know if that's bothersome for your design, but in mine, I use custom requests for each controller, so I had to do it anyway.

What is the proper way to abstract common functionality in Symfony 2 controllers

We have a fairly large symfony2 code base. Generally our Controller actions would look something like
public function landingPageAction(Request $request) {
//do stuff
return $this->render("view_to_render", $template_data);
}
We have two functionalities that are very generic between all of our controllers:
We tend to pass Controller level template parameters to all of the actions in a specific controller - let's call these "Default Parameters"
We set HTTP cache headers at the end of each Action
Understandably we want to abstract this logic away. In doing so we came up with two approaches. We are not certain which approach is better, both in terms of general OO and SOLID principles, but also in terms of performance and how SF2 recommends things be done.
Both approaches rely on having the controller extend an interface that indicates if the controller has "Default Parameters" (later we are considering also adding Cacheable interface)
use Symfony\Component\HttpFoundation\Request;
interface InjectDefaultTemplateVariablesController {
public function getDefaultTemplateVariables(Request $request);
}
Approach 1
This approach is based on events. We define an object that will store our template variables, as well as (in the future) cache indicators
class TemplateVariables {
protected $template_name;
protected $template_data;
public function __construct($template_name, $template_data) {
$this->template_name = $template_name;
$this->template_data = $template_data;
}
/**
* #param mixed $template_data
* #return $this
*/
public function setTemplateData($template_data) {
$this->template_data = $template_data;
return $this;
}
/**
* #return mixed
*/
public function getTemplateData() {
return $this->template_data;
}
/**
* #param mixed $template_name
* #return $this
*/
public function setTemplateName($template_name) {
$this->template_name = $template_name;
return $this;
}
/**
* #return mixed
*/
public function getTemplateName() {
return $this->template_name;
}
}
We also define events that will be triggered on render and which call the views
class InjectDefaultTemplateVariablesControllerEventListener {
/** #var DelegatingEngine */
private $templating;
private $default_template_variables;
public function __construct($templating) {
$this->templating = $templating;
}
public function onKernelController(FilterControllerEvent $event) {
$controller = $event->getController();
if (!is_array($controller)) {
return;
}
if ($controller[0] instanceof InjectDefaultTemplateVariablesController) {
$this->default_template_variables = $controller[0]->getDefaultTemplateVariables($event->getRequest());
}
}
public function onKernelView(GetResponseForControllerResultEvent $event) {
$controller_data = $event->getControllerResult();
if ($controller_data instanceof TemplateVariables) {
$template_data = (array)$controller_data->getTemplateData();
$template_data = array_merge($this->default_template_variables, $template_data);
$event->setResponse($this->templating->renderResponse($controller_data->getTemplateName(), $template_data));
}
}
}
Finally our Action now becomes
public function landingPageAction(Request $request) {
//do stuff
return new TemplateVariables("view_to_render", $template_data);
}
Approach 2
This approach is based on putting the common logic into a BaseController from which every other controller inherits. We are still keeping the approach of having Child controllers also extend an interface in case they want to use "Default Parameters".
The following is the new method in the base controller that determines if Default Parameters need to be merged with the specific template parameters. Later this method will also handle cache headers using ttl parameter.
public function renderWithDefaultsAndCache($view, array $parameters = array(), Response $response = null, $ttl = null)
{
$default_template_variables = array(); 
if ($this instanceof InjectDefaultTemplateVariablesController ) {
$default_template_variables = $this->getDefaultTemplateVariables();
}
$template_data = array_merge($default_template_variables, $parameters);
return $this->render($view, $template_data, $response);
}
Action now becomes
public function landingPageAction(Request $request) {
//do stuff
return $this->renderWithDefaultsAndCache("view_to_render", $template_data);
}
Discussion
So far the main arguments for the first approach were that it follows SOLID principles and is easier to extend - iin case more common logic were to be added, it can be put directly into Event Listeners without affecting the controllers.
The main arguments for the second approach were that the logic we are trying to abstract away actually does belong to the controller and not an external event. In addition there was a concern that using events in this manner will result in a poor performance.
We would be really grateful to hear from the experts on which approach is better or possibly suggest a third one that we have missed.
Thank you!
First off I am in no way claiming to be a Symfony 2 architecture expert.
I have a game schedule program which outputs a number of different types of schedules (public, team, referee etc). The various schedules are all similar in that they deal with a set of games but vary in details. The schedules need to be displayed in various formats (html,pdf,xls etc). I also wanted to be able to further tweak things for individual tournaments.
I originally used your second approach by creating a ScheduleBaseController and then deriving various individual schedule controllers from it. It did not work well. I tried to abstract common functionality but the schedules were just different enough that common functionality became complicated and difficult to update.
So I went with an event driven approach very similar to yours. And to answer one of your questions, adding some event listeners will not have any noticeable impact on performance.
Instead of focusing on template data I created what I call an Action Model. Action models are responsible for loading the games based on request parameters and (in some cases) updating the games themselves based on posted data.
Action models are created in the Controller event listener, stored in the request object and then passed to the controller's action method as an argument.
// KernelEvents::CONTROLLER listener
$modelFactoryServiceId = $request->attributes->get('_model');
$modelFactory = $this->container->get($modelFactoryServiceId);
$model = $modelFactory->create($request);
$request->attributes->set('model',$model);
// Controller action
public function action($request,$model)
{
// do stuff
// No template processing at all, just return null
return null;
}
// KernelEvents::VIEW listener
$model = $request->attributes->get('model')
$response = $view->renderResponse($model);
So the controller is mostly responsible for form stuff. It can get data from the model if need be but let's the model handle most of the data related stuff. The controller does no template processing stuff at all. It just returns null which in turn kicks off a VIEW event for rendering.
Lot's of objects? You bet. The key is wiring this up in the route definition:
// Referee Schedule Route
cerad_game__project__schedule_referee__show:
path: /project/{_project}/schedule-referee.{_format}
defaults:
_controller: cerad_game__project__schedule_referee__show_controller:action
_model: cerad_game__project__schedule_referee__show_model_factory
_form: cerad_game__project__schedule_referee__show_form_factory
_template: '#CeradGame\Project\Schedule\Referee\Show\ScheduleRefereeShowTwigPage.html.twig'
_format: html
_views:
csv: cerad_game__project__schedule_referee__show_view_csv
xls: cerad_game__project__schedule_referee__show_view_xls
html: cerad_game__project__schedule_referee__show_view_html
requirements:
_format: html|csv|xls|pdf
Each part is broken up into individual services which, for me at least, makes it easier to customize individual sections and to see what is going on. Is it a good approach? I don't really know but it works well for me.

Creating and persisting a new entity instance inside an entity

I have an entity that needs to return an instance of another entity - the stored record or a new one if one has not been stored.
<?php
/**
* #Entity
*/
class User {
/**
* #OneToOne(targetEntity="Basket")
* #JoinColumn(nullable=true)
*/
protected $activeBasket;
public function getActiveBasket() {
if (!isset($this->activeBasket)) {
$this->activeBasket = new Basket($this);
// $em->persist($this->activeBasket);
// $em->flush();
}
return $this->activeBasket;
}
}
My problem is that I don't have an EntityManager to use to persist this new Basket (and obviously don't want one in the Model). I'm not sure as to the best way to do this. I do want to be able to call $user->getActiveBasket() and retrieve a basket, whether it's previously created or a new one.
It feels like this should be a common problem, or that there's a better way to structure this (I'm hoping there's a way to hook one in in an EntityRepository or something). How can I do this?
I wouldn't return a new Basket() when it is null or not set. The model (entity) should only serve for setting and getting its properties.
The logic when $user->getActiveBasket() returned null should be elsewhere - in controller or in entity's reporitory object...
So I would move public function getActiveBasket() { ... } into just public function getActiveBasket() { return $this->activeBasket; } and somewhere in the controller I would do:
$if(!$user->getActiveBasket()) {
$user->setActiveBasket(new Basket($user));
$this->_em->persist($user);
$this->_em->flush();
}
// now do something with that activeBasket...

OOP approach in PHP

I'm programming in PHP procedurally (is this even a word?) for about five years now and decided to try an OOP approach but ran into some concept/design problems. Let's say you have some modules in the program, every module has the possibility to list, add, edit and delete an entity. An entity can be..dunno, user, client, product etc.
How would you design the classes to manipulate these entityes?
Two possibilities came in my mind:
create classes for every entity with methods like getUsersList, addUser, editUser, delUser
This approach seems resource consumingbecause in the script for the listing you only need the getUsersList and maybe delUser methods, while in the add user popup script, you only need the addUser method and in the edit user popup script only the editUser method. So, you have to instanciate an object and only use two or one of it's methods...
create general classes: listing, add, edit and delete and extend them for every entity this way you only have to instanciate one class at a time (the one you really need)
Thanks in advance,
I would create an interface defining your list, add, edit, and delete methods. This gives you a class "template". If your classes (User, Client, Product, etc.) implement this interface, then the methods in the interface must be defined in those classes.
This will give you a similar "API" to access all the functionality of every class that implements your interface. Since each of your listed objects contains different data, the details of the methods will be different, and thus separate, but the interface will be the same.
Aside:
Your inclusion of "list" in your list of methods concerns me a little. It seems to imply that you are seeing your objects as collections of Users, Clients, Products, etc, where there should most likely be a User class that represents a single user, a Client class that represents a single client, etc.
On the other hand, "list" may be handled as a static method - a method that can be called without an instance of the class.
$bob = new User('bob');
$bob->add(); // Adds bob to the database
$fred = new User('fred');
$fred->add(); // Adds fred to the database
$users = User::list(); // Gives an array of all the users in the database
That's how I would handle things, anyway.
You will need to create a solid architecture and framework for managing your data model. This is not easy and will only get more complex as the data model grows. I would highly recommend using a PHP framework (Symfony, CakePHP, etc), or at least, an ORM (Doctrine, Propel, etc).
If you still want to roll your own, I would start with an architecture similar to below.
You will want a DbRecord class that is used for individual record operations (like saving, deleting, etc). This DbRecord class will be extended by specific entities and will provide the foundation for basic entity operations.
class DbRecord {
public function save() {
// save logic (create or update)
}
public function delete() {
// delete logic
}
// other record methods
}
class User extends DbRecord {
private $name;
private $email;
public function setName($name_) {
$this->name = $name_;
}
public function setEmail($email_) {
$this->email = $email_;
}
}
From which, you can perform individual record operations:
$user = new User();
$user->setName('jim');
$user->setEmail('jim#domain.com');
$user->save();
You will now want a DbTable class that is used for bulk operations on the table (like reading all entities, etc).
class DbTable {
public function readAll() {
// read all
}
public function deleteAll() {
// delete all logic
}
public function fetch($sql) {
// fetch logic
}
// other table methods
}
class UserTable extends DbTable {
public function validateAllUsers() {
// validation logic
}
// ...
}
From which, you can perform bulk/table operations:
$userTable = new UserTable();
$users = $userTable->readAll();
foreach ($users as $user) {
// etc
}
Code architecture is the key to a website scaling properly. It is important to divide the data model into the appropriate classes and hierarchy.
Again, as your website grows, it can get very complicated to manage the data model manually. It is then when you will really see the benefit of a PHP framework or ORM.
NOTE: DbRecord and DbTable are arbitrary names - use w/e name you like.
Use your first method, where you create a reusable object with methods. It is not a waste of time as you only code it once.
class User {
function __construct() { /* Constructor code */ }
function load($id) { ... }
function save() { ... }
function delete() { ... }
}
You're on the right track with 'general classes' (also called base classes, or abstract classes in case their behaviour NEEDS to be complemented by child classes before they can be put to use).
The OOP approach would be to put all behavior that is common to all entities in the base classes.
If you use something akin to ActiveRecord, you already have a general (abstract) interface for create-update-delete operations. Use that to your advantage, and let your base classes operate ONLY on those interface methods. They don't need to know they are updating a Product, or a a User, they just need to know they can call the update() method on an entity.
But even without using something quite feature-heavy like an AR framework (check out Doctrine if you're interested in a very flexible ORM..) you can use interfaces to abstract behavior.
Let me give you a more elaborate example...
/**
* Interface for all entities to use
*/
interface Entity {
static function newEntity();
static function fetch($id);
function save();
function setProperties(array $properties);
function delete();
}
/**
* A concrete product entity which implements the interface
*/
class Product implements Entity {
public $productId;
public $name;
public $price;
public $description;
/**
* Factory method to create a new Product
*
* #param integer $id Optional, if you have auto-increment keys you don't need to set it
* #return Product
*/
public static function newEntity($id=NULL) {
$product = new Product();
$product->productId = $id;
return $product;
}
/**
* Factory method to fetch an existing entity from the database
*
* #param integer $id
* #return Product
*/
public static function fetch($id) {
// make select with supplied id
// let $row be resultset
if (!$row) {
return NULL; // you might devise different strategies for handling not-found cases; in this case you need to check if fetch returned NULL
}
$product = new Product();
$product->productId = $id;
$product->name = $row['name'];
$product->price = $row['price'];
$product->description = $row['description'];
return $product;
}
/**
* Update properties from a propreties array
* #param array $properties
* #return void
*/
public function setProperties(array $properties) {
$this->name = $properties['name'];
$this->price = $properties['price'];
$this->description = $properties['description'];
}
public function save() {
// save current product properties to database
}
public function delete() {
// delete product with $this->productId from database
}
}
/**
* An abstract CRUD controller for entities
*/
abstract class EntityCrudController {
protected $entityClass = 'UNDEFINED'; // Override this property in child controllers to define the entity class name
protected $editTemplate = NULL; // Override this to set an edit template for the specific entity
protected $templateEngine; // Pseudo-Templating engine for this example
/**
* Display the edit form for this entity
* #param integer $entityId
* #return string
*/
public function editAction($entityId) {
// Fetch entity - this is not the most clean way to fetch, you should probably consider building a factory that encapsulates this.
$entity = call_user_func($this->entityClass, 'fetch', $entityId);
// Assign entity to your edit template, in this example I'm assuming we're using a template engine similar to Smarty
// You can generate the HTML output in any other way you might like to use.
$this->templateEngine->setTemplate($this->editTemplate);
$this->templateEngine->assign('entity', $entity);
return $this->template->render();
}
/**
* Update an existing entity
*
* #param integer $entityId
* #param array $postArray
* #return string
*/
public function updateAction($entityId, array $formArray) {
// Be sure to validate form data first here, if there are errors call $this->editAction() instead and be sure to set some error information
$entity = call_user_func($this->entityClass, 'fetch', $entityId);
$entity->setProperties($formArray);
$entity->save();
// Again, using our imaginary templating engine to display...
$this->templateEngine->setTemplate($this->editTemplate);
$this->templateEngine->assign('entity', $entity);
$this->templateEngine->assign('message', 'Saved successfully!');
return $this->template->render();
}
// Devise similar generic methods for newAction/insertAction here
}
/**
* Concrete controller class for products
* This controller doesn't do much more than extend the abstract controller and override the 2 relevant properties.
*/
class ProductCrudController extends EntityCrudController {
protected $entityClass = 'Product';
protected $editTemplate = 'editProduct.tpl';
}
// Usage example:
// Display edit form:
$controller = new ProductCrudController();
$htmlOutput = $controller->editAction(1);
// Save product:
$htmlOutput = $controller->updateAction(1, array('name' => 'Test Product', 'price' => '9.99', 'description' => 'This is a test product'));
Of course, there is much to improve.. e.g. you generally don't want to make a query everytime you call fetch() on an entity, but instead only query once and store the resulting object in an IdentityMap, which also ensures data integrity.
Hope this helps, got a bit more than I intended, but I think it's commendable you try to tackle this without throwing a framework on the problem :)

Categories