Best Practices on Add-Ons with models that are pages
PermalinkIs there somewhere I can learn the best way to make addons where the model is a page w/ attributes? What I would ideally like is to have a model/class that represents my object, say a restaurant, and then modify that object. When I save it, it build a page representing that restaurant. When I load it, it loads all the page info and attributes and populates my object's properties with the attribute values. THis way, other developers working with the Restaurant object don't have to think about whether the object is backed by its own table, or a concrete page + attributes. THey just deal with an Object model.
Thanks,
Craig
Thanks,
Craig
Long answer...
There's a really long discussion thread about creating an object model for encapsulating a bunch of attributes that are *not* tied to a page:
http://www.concrete5.org/community/forums/customizing_c5/new-object...
(and it looks like Isaac submitted a pull request on github which is probably the most up-to-date version of his work:https://github.com/concrete5/concrete5/pull/71... )
But it sounds like you're asking for the opposite -- a way to encapsulate data that *is* tied to a page. I don't think there's really anything you need to do here, this is just the way C5's architecture works by default. Set up a page type in your theme, add attributes for it, probably make a custom template or two for the Page List block to display that info in other places on the site (or use the "Page List Teasers" custom template to display actual content from those pages elsewhere on the site --http://www.concrete5.org/marketplace/addons/page-list-teasers... ).
The way that things are "supposed to work" is that you don't have a dashboard interface at all, but instead you add pages of the appropriate page type and manage them as any other page (use the sitemap to re-order them, etc.). That being said, there are some situations where creating pages and such from a dashboard interface does make more sense (especially when the page is primarily data as opposed to a designed layout of visual information). To do this, you will need to either code it up yourself (seehttp://www.concrete5.org/documentation/how-tos/developers/build-a-s... for a decent tutorial, although it doesn't create pages, but does have a lot of info for how to work with C5's MVC routing, which is kind of different and not as advanced as most of the lower-level MVC frameworks out there). OR, you can use a new feature of the newly-released Concrete5 version 5.4.2 called "Composer", which provides a dashboard interface for editing certain page types and lets you set which attributes will show up during the add/edit process, among other things.
Hope that helps clear things up.
-Jordan
ChadStrat(aka-chadstreet :-)
I am definitely in this case trying to use "Pages" with attributes as my object model, the way Chadstreet is describing. I agree an object is an object regardless of how its stored. I just want to wrap the pages and attributes in a nice clean object model. So instead of something like this:
//create the page with title & description $venue = $parent->add(CollectionType::getByHandle('left_sidebar'), array('cName'=>"some title",'cDescription'=>"some desc")); //set a custom attribute $venue->setAttribute('venue_address1', "1234 somewheres ave");
I would instead do this:
$venue = new Venue $venue->title = "some title"; $venue->description = "some desc"; $venue->address1 = "1234 somewheres ave"; $venue->save();
I can see situations where I just want a data model that I don't want tied to page, because its going to be used outside the cms or i'm integrating with an existing database, but in that case i would probably just use adodb's activerecord ORM natively.
Also, put considerable focus on extending the page_list model. I didn't do this earlier on, and I regret that. I have a dealership app that approaches the page list as an "extended" model, "dealership_list extends page_list" ect. creating dozens of custom sorting and filtering methods to stack on top of the standard useful page list methods.
This versus a custom object or even custom list model in and of itself saves dozens of hours alone.
C
I updated my demo concrete5 site for our company here and composer looks really cool! Much like sharepoint. However, there doesn't seem to be any built in way to display attributes on the page with a page block. I guess you still have to code that by hand.
also, whenever you click on "write" and add a new whatever, it saves a new page as a draft even if you don't click save. Kinda buggy but its beta.
I'm going to attach what I have so far. It the beginning of a very small venue module for restaurants and bars. I'm posting it here both to get code review from you guys and also to see if this helps anyone else on how to wrap up a page w/ attributes in a nice object model.
Chad, I'm thinking of past conversations you and I have had as well.
The venue module is something we've started to display and admin restaurants and bars. It does the following:
- lets the user add/edit/list venues with name, description and address1 fields in the dashboard
- each venue becomes a page under a "venues" parent page which gets a "venue_body" block added to it for displaying the fields.
- the add-on uses "fat model, skinny controller" logic -- so the saving of the pages and attributes is done in a "venue" class.
- the venue_body block gets the current page, and loads the friendly venue model object model for display.
There's no validation, delete, etc.
Craig
done
Warning: Cannot modify header information - headers already sent by (output started at /Users/chad_cantrell/Sites/sandbox.concrete5.4.2/packages/venue/controllers/dashboard/venue.php:7) in /Users/chad_cantrell/Sites/sandbox.concrete5.4.2/concrete/libraries/controller.php on line 350
There are some style things I'd do differently but I think those are more subjective (for example, I have moved away from creating getter and setter methods in my classes and just have public variables instead -- I've never run into a real-world case where this caused problems). I also think Chad's advice about extending the PageList methods is spot on.
Definitely going to think about this approach for future projects that require similar functionality.
-Jordan
I just find it handy if I need to log access to a property or change it so that it does some logic instead of just returning or setting a value, its already setup that way. Also, i'm new to PHP so there may be a cleaner way to make them. In ruby, you can stub out getters/setters like this and change it to something more complex later:
attr_accessor :addres1, :address2, :id, :description, :etc
I was also thinking it would be cool to create a base class to map an object to a page/attributes the way activerecord maps an object to a table.
As far as I know, there's nothing like the attr_accessor/attr_reader/attr_writer stuff in php (although there are magic methods for getters, setters, and properties that can handle calls you haven't explicitly defined in your code -- but I don't think this is applicable to your situation here).
As for the activerecord-like base class, I think that's a great idea. I wonder how you'd tell it which attributes to map to properties though -- maybe the name of the class would match an attribute category, and then it would look for all attributes in that category?
Let me know if you start working on this, I'd be happy to help with it.
(Although I'd be even more interested in getting some auto-generated dashboard scaffolding going for these kinds of models -- after developing with Rails it just kills me to have to manually write form processing and validation code again).
-Jordan
I agree on the generators! That looks like where they are going with composer, except I don't see you really being able to customize the model with your own logic that way. I'd like to build a rails-like generator that would create your model object with activerecord inherited mapping to the page/attributes, and some stubbed blocks and admin tools. That would be gravy.
re: composer -- yeah, I'm hoping that actually the need for creating custom dashboard interfaces will be moot now that Composer exists (although there's always exceptions for data that doesn't have anything to do with pages).
And by the way, not sure if you've seen this yet but I've already created a generator for blocks:http://concrete5.org/marketplace/addons/designer-content...
I loath this statement with deep deep passion.
C
I guess I was referring to the customizations I have to do for sites I build for my clients, where really the end result is just a page with some data on it that I'm then pulling into a page list block on another page. But Chad's addons provide a lot of useful functionality besides just the dashboard interface.
And at more philosophical level, businesses should be about solving people's problems, and there will always be more problems that people have (and Chad, as a C5 expert, you are well positioned to take advantage of those in my opinion).
Okay I'll shut up now.
I agree. I love Composer. I really see the value of it for simple things like news and standardized basic content needs.
Look, at the end of the day, there are two types of people - those who sink, and those who swim.
My hope is to be a good swimmer.
C
I agree that Add-ons like Chad makes will always be necessarily IMHO because the sheer amount of the logic requires programming. As much as they try to get business people to "avoid programmers" using click and drag type interfaces, the issue is not having to deal with a text based language syntax like PHP or Ruby. The issue is always the complexity of the logic itself which requires programmers who think out that logic. So eventually the business people get sick of dealing with their "codeless gui" tool and ask the programmers to do it. Plus we have found that that such logic is easiest to manage and maintain, not on a GUI like Composer or Sharepoint, but in text files with IDE and SCM tools.
C
class CarsList extends PageList { /** * Filters by Model * @param model */ public function filterByModel($model) { $this->filter(false, "ak_dealership_make_model LIKE '%$model%'"); } }
So in this example, instantiating a new CarList() will by default instantiate a new PageList() as well as a new ItemList() (which is what page_list extends). Inherency is great!
So, as such, you start out off the bat having access to all things page_list related, plus your custom methods.
So you could call standard page_list functions from your custom list:
$cl = New CarList(); $cl->setItemsPerPage($this->num); //a standard page_list function $cl->filterByModel($model);//a custom cars function $cl->get(); //a standard page_list function
Which also includes pagination database_item_list functions via inherency as well.
Like a said...a TOTAL time saver.
ChadStrat
But yeah, you don't "have" to use page_list methods...but they would be there if you need them. ideal in the context of pages. Or you could simply just extend the ItemList() class and skip the page specific methods. Pagination comes from that anyway. (although I think you have to have a bridge function)
Point being, filtering and sorting, and even pagination, should be contained in a list model separate from your base object class. In that, dealing with a single venue...use the venue object. dealing with a list of sortable venues, use the venue_list object, which taps in the the item_list object.
C
So ideally, i'd like a custom PageList to return the venue objects that wrap their pages.
I think what Chad is suggesting is that you extend the PageList class and add in all of your extra attribute getters/setters and logic to the extension of that. Then you're using all of the PageList functions to work with lists, pagination, etc. -- but also will have access to the attributes and whatever custom functions you've added on in your own class.
At least I think that's what he's suggesting (and probably what I'd suggest too)
And now that I think about it, ItemList is primarily for constructing database queries (I believe -- could be wrong though), so you might be better off just writing your own functions to do the retrieval (which could use the PageList class under the hood I suppose but yeah that wouldn't be exposed to the caller).
Mike