Best practices for managing several specialized versions of one app - php

I have a web application that has many faces and so far I've implemented this through creating themes. A theme is a set of html, css and images to be used with the common back end.
Things are laid out like so:
code/
themes/theme1
themes/theme2
And each instance of the web application has a configuration file that states which theme should be used. Example:
theme="theme1"
Now new business rules are asking me to make changes to certain themes that can't be achieved through simply change the html/css/images and require changing the backend. In some cases these changes need to be applied to a group of themes.
I'm wondering how to best lay this out on disk, and also how to handle it in code. I'm sure someone else must have come up against this.
One idea is to have:
code/common
code/theme1
code/theme2
themes/theme1
themes/theme2
Then have my common code set the include_path such that code/theme1 is searched first, then code/common.
Then if I want to specialize say the LogoutPage class for theme2, I can simply copy the page from code/common to the same path under code/theme2 and it will pick up the specialized version.
One problem with this idea is that there'll be multiple classes with the same name. Although in theory they would never be included in the same execution, I wouldn't be able to extend the original base class.
So what if I was to make a unique name for the base class? e.g. Theme1LogoutPage extends LogoutPage. One problem I can foresee with that is when some common code (say the Dispatcher) references LogoutPage. I can add conditions to the dispatcher, but I wonder if there's a more transparent way to handle this?
Another option I can think of is to maintain separate branches for each theme, but I think this could be a lot of work.
One final thing to consider is that features might originate in one theme and then require merging into the common codebase.
Any input greatly appreciated. If it makes any difference, it's a LAMP environment.

I don't have a specific recommendation. However, I strongly suggest to NOT take shortcut... Use the solution that will you will find comfortable to add a third theme or to change something next year.
Duplication is the enemy of maintainability.

I'd investigate using the Strategy pattern as a means to implement different functionality in different versions of the site. Have a Factory that takes in your configuration and supplies the appropriate code strategy based on it. Each strategy can implement some common interface so that they are interchangeable from the calling class' point of view. This will isolate your changes to implement new strategies to the Factory class, Configuration class, and any new strategy classes that you need to implement to make the change. You could do the same (or similar) with any user controls that need to differ between the different versions.
I'll illustrate with pseudocode (that may look suspiciously like C#)
public interface ILogoutStrategy
{
void Logout();
}
public abstract class AbstractLogoutStrategy : ILogoutStrategy
{
public virtual void Logout()
{
// kill the sesssion
}
}
public class SingleSiteLogoutStrategy : AbstractLogoutStrategy
{
public void Logout()
{
base.Logout();
// redirect somewhere
}
}
public class CentralAuthenticationSystemLogoutStrategy : AbstractLogoutStrategy
{
public void Logout()
{
base.Logout();
// send a logout request to the CAS
// redirect somewhere
}
}
public static class StrategyFactory
{
public ILogoutStrategy GetLogoutStrategy(Configuration config)
{
switch (config.Mode)
{
case Mode.CAS:
return new CentralAuthenticationSystemLogoutStrategy();
break;
default:
case Mode.SingleSite:
return new SingleSiteLogoutStrategy();
break;
}
}
}
Example usage:
ILogoutStrategy logoutStrategy = StrategyFactory.GetLogoutStrategy( config );
logoutStrategy.Logout();

Are you using Master Pages? If you need different layout and UI stuff you could just have a different set of master pages for each of your instances. If you need custom behavior then you might want to look into Dependency Injection. Spring.NET, etc.

What you need are templates.
Thence you can separate your code from your presentation.
I highly recommend smarty templates. Also PEAR template_it.
http://www.smarty.net/
This also make your code far more maintainable. The aim is to have no html in your php, and to have no php in your html.
then all you will need to do is change the html template that is being used for each theme. or folder of templates.

You could have:
/common/code
And:
/sitename/code
All files in /common/code are abstract classes.
For every file in /common/code, just create a corresponding non-abstract class file in /sitename/code that INHERITS from the abstract class in /common/code.
This way you only need to implement CHANGES in the /sitename/code
Everything else is core functionality that exists only in /common/code
The important thing to do here is ensure that you only add public methods to the abstract classes. This way the methods are available to all sites, and classes from all sites can be treated/worked with identically.

I would do:
[theme name]/[subfolder]
default/common
default/common/html
default/common/css
red/code
red/common
red/common/html
red/common/css
red/code
green/common
green/common/html
So if the code or any other component doesn't exist it will fall back to default.
But in fact I would branch the website in svn, so common code if it evolves I can merge it, etc.. see Subversion at: http://subversion.tigris.org/

Related

Is the current way of building in PHP, the html layout structure possible?

I started not long ago with building my own mvc structure in PHP.
I have seen many people include in home.php page the header.php and footer.
I am currently stuck in finding a solid way to render my views.
I would like to know if it is even possible the way I am combining the header.php, home.php, and footer.php because this is not working for me which made me curious if there is even a native clean way of working with PHP layout structures?
any info would be appreciated. I try my best to explain the code below.
now working with this MVC structure. the router currently checks the req url and gives the controller
example of very basic router:
2 parameters: first the route, second the controller witch should render the view.
public function get($route, $controller) {
if($_SERVER['REQUEST_METHOD'] !== 'GET') {
return false;
}
$uri = $_SERVER['REQUEST_URI'];
if($uri === $route) {
$this->handled = true;
return include (controllers . $controller);
}
}
the routes that are being called:
$router = new Router();
$router->get('/', 'home.contr.php');
$router->get('/home', 'home.contr.php');
$router->get('/about', 'about.contr.php');
$router->get('/portfolio', 'projects.contr.php');
the router calls the controller and in my controller I render the view. with CreateView function
Home.contr.php:
class Home extends Controller {
public function __construct() {
Home::CreateView('home');
}
}
$home = new Home();
the extend controller that should implement the views/layout:
class Controller {
public static function CreateView($viewName) {
require_once views . 'components/header.php';
require_once views . "$viewName.php";
require_once views . "components/footer.php";
}
}
thank you in advance.
Limitations
MVC is a very broad topic with much different understanding so that by the term alone this is hard to answer properly - even in context of a PHP application. You normally refer to an existing implementation of MVC which is not the case here as you want to do it your own (Hint: Read code of existing implementations that is available and about you want to learn more).
Discussion
With that being said, you can find some practical "first next steps" suggestions at the end of the answer.
But I read your question as well that you're concerned about the HTML templates and perhaps also what this has to do with how you wrote your example. So I start a non-binding discussion about the View and then go over to Route and Controller. The Model layer I've kept out of the discussion mainly, at least for that you have to face third-party libraries as otherwise your application structure would not be a good host for broad functionality, this is touched by autoloading.
I have no authority in MVC, I just used some of the early implementations in PHP and applications influenced by them but never implemented it fully. So don't read out any suggestion from the discussion regarding it, it is merely about your example and what came to my mind in specific to PHP. At the end of the day it is you who will find the answer to your own programming questions.
Let's go.
A suggestion/assumption first: You certainly don't want to implement the view creation with the Controller class but with a View class. It would not change much just that the controller does not "care" about it (MVC = Model View Controller).
You can refactor (change) your code by introducing a View class and move the Controller::createView() to View::create() (compare: extract/move method).
Then using require_once - while it may work - it would only work if the template file is only used once. This is certainly not what you want to express here (and later in the discussion we'll see that with the existing example this can also more easily happen than perhaps intentionally thought), instead use require (or include depending on how you want to handle errors) as they will always execute the code in the file (for potential problems redefining controllers, see later in the discussion first routing and then second autoloading).
Apart from obvious code errors (typos) you'd need to address to get it to run (which is a good opportunity to explore PHP error handling and monitoring for your application) you still need to pass the output data of the controller to the view.
This can be so called view models or just objects (in the broader sense) holding the data to be viewed (rendered by the view). Just require/include-ing the (HTML layout) template files won't suffice as they may contain the HTML structure but not the controllers' output data. On the level of the templates this is typically in variables, e.g. the title of the hypertext document:
<title>
<?= htmlspecialchars($title, ENT_QUOTES | ENT_HTML5) ?>
</title>
If this would be the body of a function, the function definition would be:
function outputHeader(string $title): void {
# ...
}
As we don't have a function by requiring the template files, this is just exemplary. However we could create a generic function that handles requiring a template file and passing the variables to the template (compare include_helper()). In that layer you can also do some ground level error handling (try {} catch (Throwable $throwable) {} etc.). For starters you could collect and group such code in the View class.
What you also likely want to prevent is to bind the view within the controllers' constructor method (Controller::__construct(), ctor in short). It forces you to have a named view - and always the same - makes the controller dependent on that view.
That would mean you couldn't configure any view to any controller. While it wouldn't make sense in most cases to allow an any-to-any relationship here in the concrete practice, it allows you to actually have layer boundaries and to not couple things too tightly (compare: Spaghetti Code 1) and to write code on a higher level (in grade of abstraction, compare Layer of Indirection).
An example in a HTTP application would be to do content negotiation. This would happen on the level of request processing (more in the Router in your example), e.g. a HTTP client requests JSON instead of HTML. Now the HTML templates wouldn't fit here. But the Controller could still do the work if not the view template would be hard-encoded.
To keep things more flexible (so you can use it to a greater extend), one benefit of the MVC model is to use (and to a certain degree somehow pass the result of) the Model by the Controller to the View. It helps you define clear boundaries between those three and keep them more apart from each other (less coupled).
The routing then could negotiate and decide what to bring together, similar as in your example for the Controller already but extended with the View (template), each route could be assigned a layout/template.
As this would work quite the same as with the controller - just for the view - let's see where the current Controller not only is standing in the way for the view but already for the routing (if you find a flaw or bug, look around, often they are not in a single place and alone).
While you already configure the routes in the router, the actual routing you've put in the Controller base-class (Controller::get($route, $controller)). Similar to the __construct() method, this makes the Controller implementation dependent on the Route and even implements the routing. This is pretty convoluted and will certainly become awkward. There is also the problem when you add more routes you loose control which one matches as the matching is done within each Controller etc. . In short, while the code may be functional, it just seems to me it can benefit to be at a different place. As it's about the routing, first place that comes into my mind would be the Router itself. The Router then could do the actual work, "do the routing":
$router = new Router(); # <-- bootstrap
$router->get('/', 'home.contr.php'); # <-- prepare
$router->get('/home', 'home.contr.php'); # <-- prepare
$router->get('/about', 'about.contr.php'); # <-- prepare
$router->get('/portfolio', 'projects.contr.php'); # <-- prepare
$router->route(); # <-- do the work here
The Routers get() method then could stay the same from the outside but you would just store the routes inside and when you invoke the route() method, that configuration is matched against your request implementation.
You could then extend the router configuration with the view name.
It would be then that you still have bound a route to a controller and a view, however you have a central location where this is done (configured/parameterized). Controller and View are more independent to each other and you can concentrate more with their own implementation than the overall wiring which now moved into the router.
Finally while being here, what your example also shows is its dependence on the file-system, you have a certain file-naming convention for the controllers and also the view templates. While it is implicitly necessary to place the code into files, at least in your example on the level of the controllers you can already rely on PHP autoloading. While you want to write everything yourself (e.g. not using a ready-made MVC library), I'd still suggest to make use of some standards, like Autoloader (PSR-4) and as being inherently lazy, make the app a Composer project (it has a composer.json file) as Composer allows you to configure the autoloader and there is a well-defined process developing with it (you can also bring in more easily third-party libraries which you'll certainly need within your application logic, so this is just forward-thinking in a good sense, just start without any requirements just using the Composer autoloader).
So instead of hard-linking controller PHP file-paths, you could say instead that a controller basically is a class definition with at least a single method that the router is able to call. With the autoloader in action, the routing configuration would only need to reference that class/method and PHP then would take care to load the class. This could be done as strings (lazy-loading) or more explicit with the First class callable syntax (PHP 8.1). A good middle-ground for starters perhaps is to have one Controller per class and require to have it an interface so that you have a contract (compare: programming against interfaces 1, 2, 3, 4, 5, 6, 7 etc.). You can then simply pass the class-name and handle the instantiation in the route() method.
$router->get(
/* route */ '/',
/* $controller */ MyApp\MVC\Crontroller\Home::class,
/* $viewName */ 'home'
);
<?php
namespace MyApp\MVC\Controller;
class Home implements Interface {
# ...
}
<?php
namespace MyApp\MVC\Controller;
interface Interface {
public function invoke(InputParameter $params): InvocationResult
}
The route() then could check for the interface to verify some class can be used as a controller (instanceof) and would know how to invoke() the controller by passing the input parameters to receive the result that can be further delegated to the template layer.
This is made possible by also introducing the InputParameter and InvocationResult implementations (classes/interfaces) that help to define the layer boundary of the Controller part.
You can then do something similar for the View layer however the output comes relatively late and you're perhaps not yet settled with it (and you may have different template "engines" depending on use-case) so I would leave it more thin and less engineered and try with the Controllers first and do the delegation in the routing until you learn more about your actual requirements (Session handling, Authentication, Content-Negotiation, Redirects etc.).
At the end of the day you have to make your own decisions here.
Next Steps Suggestions
Add at least one test-script that you can run from your development environment "with a single key-press / click" and simple OK/Fail result (e.g. a simple PHP script that you execute in the shell)
Think about how to improve the error handling so you learn about defects faster (e.g. introduce exception and
Fix the bugs first, your code should actually run first of all (it might not produce the intended results in full but it should at least run - your example does not)
Init Composer / add composer.json to your project
Then change the code to your liking which can benefit having it under test first (compare Unit Tests)

Creating Composer Package

I'm trying to create a composer package & i understand the basic workflow like of creating composer.json, auto loading and creating classes under src directory.
There is one small programming misunderstanding i have is that almost all the other packages i'm reading has interfaces and a class implementing them. I don't understand need of interfaces in this context and why we need them. I have never used interface or i'm not sure if i understand its general use case. It would be nice if someone can help me understand it.
Beside the other question i had in context to composer is how do i test / run a composer project whilst i create it?
Beside this projects that i'm referring has a command directory inside src i don't understand significance of this or its use case too. I guess it has something to do with symfony php console command.
Also there is a bin directory at source, now how is that useful.
Sorry if i'm being naive here but i'm just trying to understand which components fall where and why is it like that. I couldn't find a composer tutorial online past creating composer.json
You are asking a lot of questions at once, but I'll try to at least address interfaces, since I believe that's the most important one.
Interfaces are mostly used with Dependency Injection. They define methods without actually caring how the methods are actually implemented. A class may depend on an interface instead of an actual (concrete) class, which allows an easy way to swap components. Below is an example of how one might use interfaces.
interface PostsInterface {
public function getPosts();
}
class JsonPostFetcher implements PostsInterface {
public function getPosts() {
// Load posts from JSON files here
}
}
class MySqlPostFetcher implement PostsInterface {
public function getPosts {
// Load posts from a MySQL database
}
}
class Blog {
public function __construct(PostsInterface $fetcher) {
// Load posts from either JSON or a database
// depending on which fetcher is provided
$posts = $fetcher->getPosts();
}
}
Using this method anyone can now write their own code to provide posts from an external API ApiPostFetcher, SQLite Database SqlitePostFetcher, Serialized PHP files SerializedPostFetcher etc. One could even write a DymmyPostFetcher that simply returns a pretermined array of posts that could be used for testing purposes. You can then use any implementation of PostsInterface in your blog like in the following example.
$fetcher = new JsonPostFetcher(); // You can provide different fetchers here.
$blog = new Blog($fetcher);
If you're unfamiliar with dependency injection, I highly recommend learning it, since it will be especially useful in writing modular code.

PHP class and method overriding - implement callbacks

I'm currently working on a project where the core system is distributed out to many different clients - and then should the client request changes, we have to make them individually on each system which means that eventually the core code varies from client to client, and keeping it up to date and copying new features across the system is difficult.
I have proposed we move to (what I'm calling) an 'override model' which has an outside skeleton structure of the code. Somewhat like:
|- controllers
|- models
|- views
|- core
|- controllers
|- Controller1.php
|- models
|- views
If you then wanted to make changes to Controller1.php, you would copy it into the outside structure and make changes - an autoloader would then load the appropriate files if they exist by checking the Skeleton structure for them first, i.e.
Loader::controller('Controller1');
However I wondered if it is possible to go a bit further than that - its all well and good overriding the Controller if changes are needed, but then any future core additions or fixes might not get added in. So I thought you could possibly create a copy of the file and override just the singular method calls. An semi-example of what I mean is as follows:
class Override {
public function __call($method, $args) {
return call_user_func_array(array('Something', $method), $args);
}
public static function __callStatic($method, $args){
return call_user_func_array(array('Something', $method), $args);
}
}
// Core class
class Something {
static function doTest() {
echo "Class something <br/>";
}
static function doOtherTest() {
echo "That works <br/>";
self::doTest();
}
}
// Overriding class - named differently for ease of example and reasons explained later
class SomethingElse extends Override {
private static function doTest() {
echo "Success <br/>";
}
}
// Actual function calling
SomethingElse::doTest();
SomethingElse::doOtherTest();
The general idea being that if the method doesn't exist in the originating class, then action it from the 'parent' class (which is hardcoded here). However I have two issues with this method:
I think I will run into trouble when the classes have the same names
If I am attempting to override a method that the parent class subsequently calls, it will use it's own version of the method as opposed to the one I am attempting to override
Although the 'simple' solution is to say you should override any methods that are in conjunction, but more might be added at a later date.
Currently I am trying to just do the initial solution of full class overriding using the loader, which works and is less complex.
However I wondered if any of the great minds on StackOverflow might know of any answer or set-up that might help address the issues with the method overriding idea - please bear in mind I'm working with an existing system set up, although the skeleton structure idea is what I am trying to implement to bring some form of 'control' over what is changed. Ideally, nothing in the core would change (at least not by much) when someone wants to override a method or similar.
Well we've just solved it. Traits it is!
But seriously by converting the versioned code to traits and then calling them in the non versioned files in the above structure. This then negates the need for a loader class and other clash prevention layers and allows the core code to be updated, tested and committed without affecting custom code per client.
well the strict OO Kind of Solution would surely be to spread your controller as abstract interfaces that could be implementet by several different real world approaches and then brought together by the composition over inheritace principle.
As i understand you've got existing code here that you intend to override or extend. If your PHP-Version allows you to use Traits, this might help you too:
PHP 5.4: why can classes override trait methods with a different signature?

Should I remove static function from my code?

My code is located here: https://github.com/maniator/SmallFry
Should I make it so that that the App class does not have to use static functions but at the same time be able to set and set variables for the app from anywhere?
Or should I keep it how it is now with App::get and App::set methods?
What are the advantages and disadvantages of both?
How would I accomplish that 1st task if I was to undertake it?
Related Question
Sample code:
//DEFAULT TEMPLATE
App::set('APP_NAME', 'SmallVC');
//END DEFAULT TEMPLAT
//
//DEFAULT TEMPLATE
App::set('DEFAULT_TEMPLATE', 'default');
//END DEFAULT TEMPLATE
//DEFAULT TITLE
App::set('DEFAULT_TITLE', 'Small-VC');
//END DEFAULT TITLE
//LOGIN SEED
App::set('LOGIN_SEED', "lijfg98u5;jfd7hyf");
//END LOGIN SEED
App::set('DEFAULT_CONTROLLER', 'AppController');
if(App::get('view')){
$template_file = $cwd.'/../view/'.App::get('view').'/'.App::get('method').'.stp';
if(is_file($template_file)){
include $template_file;
}
else {
include $cwd.'/../view/missingview.stp'; //no such view error
}
}
else {
App::set('template', 'blank');
include $cwd.'/../view/missingfunction.stp'; //no such function error
}
I think you have a feeling that static is bad. What I am posting may seem fairly crazy as it is a massive change. At the very least hopefully it presents a different idea of the world.
Miško Hevery wrote static methods are a death to testability.
I like testing, so for that reason I don't use them. So, how else can we solve the problem? I like to solve it using what I think is a type of dependency injection. Martin Fowler has a good but complicated article on it here.
For each object at construction I pass the objects that are required for them to operate. From your code I would make AppController become:
class AppController
{
protected $setup;
public function __construct(array $setup = array())
{
$setup += array('App' => NULL, 'Database' => NULL);
if (!$setup['App'] instanceof App)
{
if (NULL !== $setup['App'])
{
throw new InvalidArgumentException('Not an App.');
}
$setup['App'] = new App();
}
// Same for Database.
// Avoid doing any more in the constructor if possible.
$this->setup = $setup;
}
public function otherFunction()
{
echo $this->setup['App']->get('view');
}
}
The dependancies default to values that are most likely (your default constructions in the if statements). So, normally you don't need to pass a setup. However, when you are testing or want different functionality you can pass in mocks or different classes (that derive from the right base class). You can use interfaces as an option too.
Edit The more pure form of dependency injection involves further change. It requires that you pass always pass required objects rather than letting the class default one when the object isn't passed. I have been through a similar change in my codebase of +20K LOC. Having implemented it, I see many benefits to going the whole way. Objects encapsulation is greatly improved. It makes you feel like you have real objects rather than every bit of code relying on something else.
Throwing exceptions when you don't inject all of the dependencies causes you to fix things quickly. With a good system wide exception handler set with set_exception_handler in some bootstrap code you will easily see your exceptions and can fix each one quickly. The code then becomes simpler in the AppController with the check in the constructor becoming:
if (!$setup['App'] instanceof App)
{
throw new InvalidArgumentException('Not an App.');
}
With every class you then write all objects would be constructed upon initialisation. Also, with each construction of an object you would pass down the dependencies that are required (or let the default ones you provide) be instantiated. (You will notice when you forget to do this because you will have to rewrite your code to take out dependencies before you can test it.)
It seems like a lot of work, but the classes reflect the real world closer and testing becomes a breeze. You can also see the dependencies you have in your code easily in the constructor.
Well, if it was me, I would have the end goal of injecting the App dependency into any class (or class tree) that needs it. That way in testing or reusing the code you can inject whatever you want.
Note I said reuse there. That's because it's hard to re-use code that has static calls in it. That's because it's tied to the global state so you can't really "change" the state for a subrequest (or whatever you want to do).
Now, on to the question at hand. It appears that you have a legacy codebase, which will complicate things. The way I would approach it is as follows:
Create a non-static version of the app class (name it something different for now) that does nothing but proxy its get/set calls to the real app class. So, for example:
class AppProxy {
public function set($value) {
return App::set($value);
}
}
For now, all it has to do is proxy. Once we finish getting all the code talking to the proxy instead of the static app, we'll make it actually function. But until then, this will keep the application running. That way you can take your time implementing these steps and don't need to do it all in one big sweep.
Pick a main class (one that does a lot for the application, or is important) that you easily control the instantiation of. Preferably one that you instantiate in only one place (in the bootstrap is the easiest). Change that class to use Dependency Injection via the constructor to get the "appproxy".
a. Test this!
Pick another class tree to work on, based on what you think will be most important and easiest.
a. Test!!!
If you have more calls to App::, Go to #3
Change the existing App class to be non-static.
a. Test!!!!!!!!!!
Remove the AppProxy and replace with App in the dependency injectors. If you did it right, you should only have one place to change to make this switch.
Pat yourself on the back and go get a drink, cause you're done.
The reason that I segmented it out like this is that once a step is completed (any step), you can still ship working software. So this conversion could take literally months (depending on the size of your codebase) without interrupting business as usual...
Now, once you're done, you do get some significant benefits:
Easy to test since you can just create a new App object to inject (or mock it as needed).
Side effects are easier to see since the App object is required wherever it could be changed.
It's easier to componentize libraries this way since their side effects are localized/
It's easier to override (polymorphism) the core app class if it's injected than if it's static.
I could go on, but I think it's pretty easy to find resources on why statics are generally bad. So that's the approach I would use to migrate away from a static class to an instance...
If you don't want to have static functions but global access from everywhere WITHOUT passing the object to the places where it is actually needed then you pretty much can only use one thing:
A global variable
So you are not really better of doing that. But that is the only thing i can think of that would fulfill your requirements.
If you App object is something like an application config a first possible step would be to pass it to the objects that need it:
class Login {
public function __construct() {
$this->_login_seed = App::get('LOGIN_SEED');
self::$_ms = Database::getConnection();
}
changes into:
class Login {
public function __construct(App $app) {
$this->_login_seed = $app->get('LOGIN_SEED');
self::$_ms = Database::getConnection();
}

How to use different connections according to query type in Yii

All my reads should go to one DB connection
All my writes should go to another connection
How do I accomplish this in Yii, with minimal changing the code of the core library?
And on occasions (as stated in the comments) I will need the ability to control each Model type of connection, so read can go to the Master too.
I have written an app where the master admin panel could be used to create and administer several customer-facing "instances", so there was the need to "direct" queries running inside the master app to any one of the instance-specific databases. I 'll illustrate a trimmed-down version what I did first (which is not as demanding as your goal) and present a more powerful approach afterwards.
Using multiple databases for all queries
Directing queries to a database that has been specified from beforehand is easy: just override the CActiveRecord::getDbConnection method. What I did can be trimmed down to this:
abstract class InstanceActiveRecord extends CActiveRecord {
public static $dbConnection = null;
public function getDbConnection() {
if (self::$dbConnection === null) {
throw new CException('Database connection must be defined to work with instance records.');
}
return self::$dbConnection;
}
}
So if you want to direct all operations to a specific database you simply have to derive your ActiveRecord models from InstanceActiveRecord instead of CActiveRecord, then just do InstanceActiveRecord::dbConnection = $connection and you are good to go.
Using multiple databases with auto-selection based on query type
For this you need to go deeper into CActiveRecord. It turns out that getDbConnection is mostly used by getCommandBuilder, which in turn is the method called by all the delete/update/insert families. So we need to pass some kind of context from those functions down to getDbConnection, where the choice of which connection we want to use will be made.
For this we 're going to have to override all methods in those families, so a reasonable approach might be:
Step 1. Add an optional parameter to getDbConnection and override it to return whichever connection you want it to based on the parameter value. The simplest would be something like this:
public function getDbConnection($writeContext = null) {
if ($writeContext === null) {
return parent::getDbConnection(); // to make sure nothing will ever break
}
// You need to get the values for $writeDb and $readDb in here somehow,
// but this can be as trivially easy as you like (e.g. public static prop)
return $writeContext ? $writeDb : $readDb;
}
Step 2. Add an optional parameter to getCommandBuilder with the same semantics and override it to forward the value:
public function getCommandBuilder($writeContext = null) {
return $this->getDbConnection($writeContext)->getSchema()->getCommandBuilder();
}
Step 3. Find all call sites of getCommandBuilder (there will be a bunch of those) and getDbConnection (there were just 2 more than the one inside getCommandBuilder at the time I looked) and override them to specify the read/write context appropriately. Example:
public function deleteAll($condition='',$params=array()) {
Yii::trace(get_class($this).'.deleteAll()','system.db.ar.CActiveRecord');
// Just need to add the (true) value here to specify write context:
$builder=$this->getCommandBuilder(true);
$criteria=$builder->createCriteria($condition,$params);
$command=$builder->createDeleteCommand($this->getTableSchema(),$criteria);
return $command->execute();
}
After this you should be ready to go. There's also nothing stopping you from making a more involved context selection mechanism than the true/false option illustrated here, the concept is the same.
Practical concerns
While all of this will achieve the stated goal perfectly, there remains a question regarding the maintainability of this approach.
It's true that going this route will involve lots of copy/pasted code from CActiveRecord, which is not ideal if there's the chance of moving your app to a later version of the framework later on; to do so, you will be forced to bring your subclass up to sync with the latest version of CActiveRecord.
To migitate this and make your life easier in the future, you can consider this approach:
Instead of copy/pasting and overriding only part of CActiveRecord, make an exact copy (minus the properties of course) of CActiveRecord and perform the changes there. In other words, copy over even those methods that you do not intend to override.
Perform the changes mentioned above. Remember that this involves an override of getDbConnection and only really minor edits to a dozen or two of other places.
Make your models extend the resulting class.
Now when the time comes to upgrade to a later Yii version, you will need to bring your class in sync with CActiveRecord again. Fire up your favorite diff tool and compare your class with the target version of CActiveRecord. The diff tool will show you only the getDbConnection and minor edits, plus whatever changes were made to CActiveRecord in Yii's core. Copy those other changes over to your class. Problem solved in 5 minutes tops.

Categories