• 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
overall code organization: helper, library, core, or model?

#1
I'm taking a largish existing web application and converting to CI3. I am wondering a few things about code organization. While I understand it's probably good to have skinny controllers and fat model classes, I'm still kinda wondering where things go. I was hoping to get some advice.

Right off the bat, it seems clear to me that I need an autoloader. We used one in the old code because we had gobs and gobs of require/include statements that contributed a lot to various scripts but which also were the source of some pretty irritating bugs. I suspect that creating a pre-system hook is probably the right way to go about this as described here, but would like to hear if anyone disagrees. I especially like ivantcholakov's example, although I expect I might limit this autoloader to only apply to classes named with a specify prefix (e.g., MY_ or similar) just to be sure I avoid name collisions or similar issues. Really hope CI adopts namespaces soon.

I'm pretty sure I hate the way the loader works. It precludes the creation of static library classes, among other things. I can appreciate that it might have worked for PHP4, but that was ages ago.

I'm also hoping folks might clarify for me what exactly a helper or library is intended to describe. Seems like helpers are basically trivial functions. I'm not really sure what is meant to go in the libraries subdirectory, although an emailer or notification class jumps to mind. Perhaps folks might comment on these specific examples?

1) Data Object classes. I.e., special classes to act as intermediaries between our code and the database. These extremely useful auto-generated classes preclude the need to write any SQL for most single-record interactions (CRUD, etc). I'm definitely thinking these belong in the models directory.

2) A registry to store site-wide configuration values such as:
* email host/user/password/options
* payment gateway keys
* cloud api keys (e.g., Amazon, Rackspace, etc.)
* other?
I can't decide where this would go. Pretty sure it's not a helper.

3) Payment and business-related classes such as a 'payment router' class. I.e., business logic that guides users through a series of purchase-related pages. More specifically, a class that helps redirect a user to the optimal page based on their account status, prior purchases, session data, and other considerations. Because this is business logic so tightly linked to our business processes and data, I'm strongly inclined to call this a model, but not 100% sure.

4) Email and notification functionality. I'm thinking this should be a library, but I hate the way CI will instantiate an instance of loaded libraries and attach this instance to the current controller. I'm thinking it should go in the library directory but be loaded via my autoloader rather than CI's loader.

5) Miscellaneous cron jobs that expire offers, purge old inventory data, import new inventory data, etc.
I know that CI can be invoked via command line so I'm thinking I'll probably try and define a Cron_Controller class that fails if any methods are invoked with SAPI != CLI. Still, some of these jobs are pretty complex so I'm worrying a bit about fat controllers.

6) Code to deploy to special-purpose, dynamically-allocated distributed computing nodes.
This website has a multi-node structure where it can spawn additional servers to perform special image-crunching tasks. I've created a really complex (i.e., non-CI) codebase which must be deployed to those external machines. Seems to me this code probably doesn't belong in any CI location at all, although the web application and these remote applications do share a few classes in common.

7) Miscellaneous code to enforce user privileges derived from user access levels, subscriptions, and purchasing behavior.
This is stuff like "don't let users bid on products unless they have an account with access_level=2" and such. I wonder if this is controller or model?

Any comments/anecdotes/wisdom/constructive criticism are welcome.
Reply

#2
CI v3 supports Composer if you enable it via a config value, so that would probably be your best bet for an autoloader, though it presumes the code you need to autoload is compatible with Composer.

I've used a number of static libraries with CI which simply include a non-static constructor for CI to call.

A helper is usually just a group of related function definitions with no class definitions. The libraries directory is more or less a catch-all for classes which do not have an easily-defined place in the MVC structure.

In my opinion, most of your examples would be libraries, but it really depends on the structure of the code itself and the way in which you would normally call the code. CI is very flexible in many ways, so different users can establish their code separation in different ways. The first three or four examples could just as easily be models, and in my own site I have a number of models involved in handling notifications, though the notifications themselves are triggered by various models (as part of an insert or update, for example) and even by a library in some situations (e.g. retrieve and display notifications when a user logs in).

There are nearly as many opinions on where to draw the line between a model and controller as there are sites using MVC (and each framework implements MVC in a way that reflects an opinion on where that line should be drawn). CI does tend to lend itself to fat controllers, and I will often start with a fat controller and thin it out over time (for instance, the data requirements for a given portion of the site may not be clear initially, so the model(s) may not be implemented until later). I tend to think that any repetition may be an indication that the repeated code is in the wrong place, but this doesn't always help clarify where the code should reside.

In your case, since much of the code may already be written, it may be best to start with setting up the routing and some controllers to interface with the existing code (utilizing an autoloader or wrapping the code into libraries and/or helpers).
Reply

#3
@sneakyimp

"2) A registry to store site-wide configuration values such as:" - this is where I started https://github.com/ericbarnes/ci-setting...ttings.php This library stores your settings within the database, storing and retrieval is by keywords.

I've got slightly different implementations, I don't need grouping of the settings, on the other hand, I need language-sensitive settings. You know where my repository is.

And within the administration panel user-interface for settings is to be created.

Before storing the email password, API keys and other security-sensitive data - encrypt them. On retrieval - decrypt them. Use a separate encryption key (not the default one) for this purpose.
Reply

#4
@sneakyimp

On the other things: Download PyroCMS (2.1.x or 2.2.x - I am not familiar with the newer ones) and Bonfire 0.7.x and review code for their solutions. I've learned from them mostly. Other ready yet systems might be helpful too - compare and choose features, adapt code, check third-party code for upgrades. Don't go making everything alone, hunt for ready features.
Reply

#5
(12-27-2014, 06:51 PM)sneakyimp Wrote: 3) Payment and business-related classes such as a 'payment router' class. I.e., business logic that guides users through a series of purchase-related pages. More specifically, a class that helps redirect a user to the optimal page based on their account status, prior purchases, session data, and other considerations. Because this is business logic so tightly linked to our business processes and data, I'm strongly inclined to call this a model, but not 100% sure.

7) Miscellaneous code to enforce user privileges derived from user access levels, subscriptions, and purchasing behavior.
This is stuff like "don't let users bid on products unless they have an account with access_level=2" and such. I wonder if this is controller or model?

It may be personal preference, but I think #3 is a model.  I try to do as little as possible in my controllers: validate the request, get info from a model, send it to the browser. I'd want something like this in my controller for #3:

$user = usermodel->get($id);   // or get it from the session or auth lib
$view = pagemodel->getOptimal($user);

For #7, it really depends on how you are doing authentication.  I like to do it in a controller constructor hook, some might do it in a MY_Controller that they extend, or you could write a custom validation rule.  You could add a hasBidAccess function to your authentication library that calls a function in a datamodel that does a query.   There are a lot of options.
 
Reply

#6
(01-02-2015, 12:53 PM)mwhitney Wrote: CI v3 supports Composer if you enable it via a config value, so that would probably be your best bet for an autoloader, though it presumes the code you need to autoload is compatible with Composer.
I'm not especially familiar with Composer, but understand that it's a command-line tool that is essentially a package manager. Could you elaborate? I expect I'll still need an autoload function regardless of whether I use composer or not.

(01-02-2015, 12:53 PM)mwhitney Wrote: I've used a number of static libraries with CI which simply include a non-static constructor for CI to call.
.
Dodgy Booooo!

(01-02-2015, 12:53 PM)mwhitney Wrote: A helper is usually just a group of related function definitions with no class definitions.
Thanks for clarification. Potentially useful, but also seems like a good way to invite name collisions.

(01-02-2015, 12:53 PM)mwhitney Wrote: The libraries directory is more or less a catch-all for classes which do not have an easily-defined place in the MVC structure.
Seems to me like 'libraries' is a good place for third-party libs. I.e., "code that is not of my creation" and also "code I created which I reuse for many disparate projects"

(01-02-2015, 12:53 PM)mwhitney Wrote: in my own site I have a number of models involved in handling notifications, though the notifications themselves are triggered by various models (as part of an insert or update, for example) and even by a library in some situations (e.g. retrieve and display notifications when a user logs in).
How do your models and libraries gain access to your libraries and models then? Seems to me the default loading behaviors in CI rely on the sharing of a fairly heavy CI object which serves as some kind of referential conduit so that the various pieces can all locate each other. It's like the CI object is a spine or hub of some kind.

(01-02-2015, 12:53 PM)mwhitney Wrote: There are nearly as many opinions on where to draw the line between a model and controller as there are sites using MVC (and each framework implements MVC in a way that reflects an opinion on where that line should be drawn). CI does tend to lend itself to fat controllers, and I will often start with a fat controller and thin it out over time (for instance, the data requirements for a given portion of the site may not be clear initially, so the model(s) may not be implemented until later). I tend to think that any repetition may be an indication that the repeated code is in the wrong place, but this doesn't always help clarify where the code should reside.
I really like how you expressed this. Well-said!

(01-02-2015, 12:53 PM)mwhitney Wrote: In your case, since much of the code may already be written, it may be best to start with setting up the routing and some controllers to interface with the existing code (utilizing an autoloader or wrapping the code into libraries and/or helpers).
Working on this. Hoping to resolve Registry and Notification functionality first.
Reply

#7
(01-02-2015, 10:04 PM)ivantcholakov Wrote: "2) A registry to store site-wide configuration values such as:" - this is where I started https://github.com/ericbarnes/ci-setting...ttings.php This library stores your settings within the database, storing and retrieval is by keywords.
Good stuff. And very similar to what I had in mind. I'd say more but feel this should be a separate topic.

(01-02-2015, 10:04 PM)ivantcholakov Wrote: I've got slightly different implementations, I don't need grouping of the settings, on the other hand, I need language-sensitive settings. You know where my repository is.
I also need language functionality. I'm thinking pretty differently about language constants (like an "are you sure?" prompt in five langages) versus Settings/Registry/Config settings (e.g., user key and secret key for a CDN or cloud gateway).

(01-02-2015, 10:04 PM)ivantcholakov Wrote: And within the administration panel user-interface for settings is to be created.
This model for settings that uses a database certainly lends itself well to this kind of convenient interface.

(01-02-2015, 10:04 PM)ivantcholakov Wrote: Before storing the email password, API keys and other security-sensitive data - encrypt them. On retrieval - decrypt them. Use a separate encryption key (not the default one) for this purpose.
If these values must be decrypted on the server, then the key to access them must also. I'm wondering why you recommend this? It would surely cause a performance penalty if commonly used values must be decrypted for each page access?
Reply

#8
(01-02-2015, 11:30 PM)bclinton Wrote: It may be personal preference, but I think #3 is a model.  I try to do as little as possible in my controllers: validate the request, get info from a model, send it to the browser. I'd want something like this in my controller for #3:

$user = usermodel->get($id);   // or get it from the session or auth lib
$view = pagemodel->getOptimal($user);
I definitely agree it should work something like this. I tend to think of the controllers as switchboard operators and the models as the entity one is calling with whom to talk business.

(01-02-2015, 11:30 PM)bclinton Wrote: For #7, it really depends on how you are doing authentication.  I like to do it in a controller constructor hook, some might do it in a MY_Controller that they extend, or you could write a custom validation rule.  You could add a hasBidAccess function to your authentication library that calls a function in a datamodel that does a query.   There are a lot of options.
 
I'm sort of torn here. It certainly seems clear that a given page view or action should be intercepted by a controller if a user is not sufficiently privileged. Viewing admin dashboard, for instance, should not be accessible except by admin. On the other hand, other actions may depend on a lot of conditions that seem squarely in the Model camp: existence of records in a particular database, values from a database, etc. The rules for access to a particular action could be quite elaborate. I've heard discussion of using Interceptor Pattern in this context, but can't yet formulate a proper vision of code and data structures. In particular, creating three base controller classes:
* Public - for pages accessible to everyone
* User - for pages accessible to registered users
* Admin - for pages only accessible to Admins.
This might work fine for a simple site but what if you want to specify a-la-carte permissions like an ACL or *nix-style file permissions? Or what if you have N user levels rather than just these 3? I'd appreciate any thoughts on code structures, data structures, and code organization if anyone has any.
Reply

#9
(01-05-2015, 06:01 PM)sneakyimp Wrote: I'm sort of torn here. It certainly seems clear that a given page view or action should be intercepted by a controller if a user is not sufficiently privileged. Viewing admin dashboard, for instance, should not be accessible except by admin. On the other hand, other actions may depend on a lot of conditions that seem squarely in the Model camp: existence of records in a particular database, values from a database, etc. The rules for access to a particular action could be quite elaborate. I've heard discussion of using Interceptor Pattern in this context, but can't yet formulate a proper vision of code and data structures. In particular, creating three base controller classes:
* Public - for pages accessible to everyone
* User - for pages accessible to registered users
* Admin - for pages only accessible to Admins.
This might work fine for a simple site but what if you want to specify a-la-carte permissions like an ACL or *nix-style file permissions? Or what if you have N user levels rather than just these 3? I'd appreciate any thoughts on code structures, data structures, and code organization if anyone has any.

If you would like, you can look at my authentication library to see how I did a simple ACL and am using a post-controller-constructor for authentication rather than multiple base controller classes:  http://forum.codeigniter.com/thread-264.html  It may give you some ideas.

It does not have complex permissions though, like what you were talking about where a permission would need to be determined by multiple attributes.  Whether you would accomplish something like this in a library or a model... you could go either way.  For entity level permissions, I usually use model functions in the entity's model.
Reply

#10
(01-05-2015, 05:01 PM)sneakyimp Wrote:
(01-02-2015, 12:53 PM)mwhitney Wrote: CI v3 supports Composer if you enable it via a config value, so that would probably be your best bet for an autoloader, though it presumes the code you need to autoload is compatible with Composer.
I'm not especially familiar with Composer, but understand that it's a command-line tool that is essentially a package manager. Could you elaborate? I expect I'll still need an autoload function regardless of whether I use composer or not.

Composer is both a package manager and an autoload function. The command-line tool reads the composer.json file in your project and pulls in the packages, then generates an autoload function for those packages. CI includes a configurable option to call the Composer autoload function, or you can call it yourself in your project's index.php file (since CI may load it later in the application than may be desired for some uses).

(01-05-2015, 05:01 PM)sneakyimp Wrote:
(01-02-2015, 12:53 PM)mwhitney Wrote: I've used a number of static libraries with CI which simply include a non-static constructor for CI to call.
.
Dodgy Booooo!

...and we could digress into a discussion of the questionable nature of static libraries in PHP, which does not support static classes in the first place. I think we can just agree that it's annoying and move on.

(01-05-2015, 05:01 PM)sneakyimp Wrote:
(01-02-2015, 12:53 PM)mwhitney Wrote: A helper is usually just a group of related function definitions with no class definitions.
Thanks for clarification. Potentially useful, but also seems like a good way to invite name collisions.

In some ways, you could think of a helper as a prehistoric namespace (or even class) mechanism, except that the functions are still defined globally. In most cases, every function definition is wrapped in an if (! function_exists()) call, and one of the more interesting features of CI is that you can define a MY_xxx_helper which provides your own definitions of functions defined in the CI xxx_helper, providing an override method for basic functions.

Yes, name collisions are a constant hazard in a CI project, but in most cases they're not as common as you might expect.

(01-05-2015, 05:01 PM)sneakyimp Wrote:
(01-02-2015, 12:53 PM)mwhitney Wrote: The libraries directory is more or less a catch-all for classes which do not have an easily-defined place in the MVC structure.
Seems to me like 'libraries' is a good place for third-party libs. I.e., "code that is not of my creation" and also "code I created which I reuse for many disparate projects"

Yes. There's also a third_party directory which can be used for many of the same purposes, but offers some additional support for overloading core classes. As with everything in CI, there is almost always more than one way to do something, and very few things have one distinct right answer.

(01-05-2015, 05:01 PM)sneakyimp Wrote:
(01-02-2015, 12:53 PM)mwhitney Wrote: in my own site I have a number of models involved in handling notifications, though the notifications themselves are triggered by various models (as part of an insert or update, for example) and even by a library in some situations (e.g. retrieve and display notifications when a user logs in).
How do your models and libraries gain access to your libraries and models then? Seems to me the default loading behaviors in CI rely on the sharing of a fairly heavy CI object which serves as some kind of referential conduit so that the various pieces can all locate each other. It's like the CI object is a spine or hub of some kind.

More often than not, I take the approach of calling $this->load->library(my_library) or $this->load->model(my_model), which attaches $this->my_library or $this->my_model to the corresponding object. If I want to avoid that, I can still use any number of other methods to load and pass objects. In most libraries, the CI object is only pulled in when it is needed, using the get_instance() function to get a reference to the CI controller. If the CI reference is heavily relied on, the library will usually define a property which will be set to the reference and used as required.

If you start digging into some of the CI core code, you can see that the spine/hub metaphor works fairly well. CI starts out by loading up several key components, then it loads the CI_Controller. The CI_Controller attaches all of the CI components which were previously loaded to itself, then loads and initializes the Loader, which loads all of the classes configured to autoload (and the loader usually attaches each loaded class to the CI_Controller instance).

If everything is done properly, CI_Controller behaves as a large singleton which gives you access to most of CI's functionality, usually exposed through $this (because a controller will generally extend CI_Controller and the CI_Model includes the __get() method which references the properties and methods exposed through get_instance()).
Reply


Digg   Delicious   Reddit   Facebook   Twitter   StumbleUpon  


  Theme © 2014 iAndrew  
Powered By MyBB, © 2002-2020 MyBB Group.