Best Practices on Add-Ons with models that are pages
Permalink
I noticed with add-ons like Chadstreets Proevents or the Concrete5 event team, they build the events as a page and use attributes to add extra fields for things like dates, images, links to a page in the site, etc. This is very different then using normal activerecord style models with a standard table for storage.
Is 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
Is 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
That is more or less what i'm trying to do but just wanted to see if there were any best practices or guides out there on how to build an add-on that creates pages with custom modules. Also, is there a base class that will map an object model to a page with attributes, or do you have to do all that yourself?
Thanks,
Craig
Thanks,
Craig
Short answer: use the new "Composer" feature of Concrete version 5.4.2.
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
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
I let user interaction drive the model. If the primary interaction is page related at all, in my opinion, there's no reason not to use the C5 page object model. An object is an object regardless of data type IMHO. And the page list model is easy enough to extend...it's sorta a no-brainer.
ChadStrat(aka-chadstreet :-)
ChadStrat(aka-chadstreet :-)
Thanks guys, I'll take a look at composer.
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:
I would instead do this:
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.
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.
I could see that making things cleaner. one thing I have been doing that seems to clean things up is grab all the attributes by set and loop through them to save. bit easier than pulling each one by name and saving.
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
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
So what is a page list?
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 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.
Hey all,
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
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
get an error:
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
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
I haven't tested this with the newest version of Concrete. I think i'm using 5.4.1. I had some problems with 5.4.2.
also try removing the "echo 'done'" in that file.
I think this is a really great idea -- thanks for sharing your code!
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
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
Well, I use getter/setters because that's the OOP idiom for encapsulation. Its usually a bad idea to make an object's internal variables public. However, encapsulation serves a greater purpose which is loose coupling between different parts of your application, so if you're accomplishing that another way I think thats fine and it also depends on the complexity of the app.
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.
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.
I understand the concept of encapsulation, I just find that in practice I've never needed that protection and it's just easier to call the member variables directly. But like I said, it's just a style thing -- and it looks like you actually know what you're talking about with OOP and understand the tradeoffs, so definitely go with what you think :)
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
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
Yeah, I'm an oop/code snob. Just ask ChadStrat. :) I'm more pragmatic these days than i used to be, especially for CMS type sites.
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.
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.
Heh... yeah using a CMS is always an exercise in compromise. I've found it to be well worth the trade-off for sites that fall within the "normal" feature set. But for custom applications I'd always go lower-level and use Rails or Kohana (if PHP is a requirement). It's a bit of a grey area with things like this, though, where 50% of the site is normal informational stuff and 50% is custom application.
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...
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'm hoping that actually the need for creating custom dashboard interfaces will be moot now that Composer exists "
I loath this statement with deep deep passion.
C
I loath this statement with deep deep passion.
C
Why?
Probably because thats a big part of how he makes his living! lol!
Understood -- yeesh, then that came off as really insensitive on my part -- sorry about that.
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 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.
nah. I'm just mess'n. lol
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. 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
You know, they have tried to make these "auto-generate your database/app here" or "drag and drop to build an application" type products, and for the most part they work in simple cases. Sharepoint tries to do this quite a bit and it works for list and document management but it fails utterly when you try to make it a platform for an entire business application. I see the same pros and cons with concrete/composer and any similar CMS's.
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.
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.
I would add a "venue_list" model that extends the standard "page_list" model. This will provide you greater flexibility with listing your venues and managing custom filtering and sorting.
C
C
yeah that sounds like a good idea. Right now I've just been using the standard pagelist, then looping over each of the results to create a venue and returning that as an array. But its probably not super performant.
Do you have any example code for that?
sure.
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:
Which also includes pagination database_item_list functions via inherency as well.
Like a said...a TOTAL time saver.
ChadStrat
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
ah so this gives you paging/sorting out of the box. However, my Venue objects do not inherit from Page. They wrap the page object. Would I still be able to use it, returning a bunch of venues from it rather than pages?
well, it's generally a good idea to separate out lists models from static objects anyway imo.
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
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
yes, but in a list of these objects, i'll want to use the venue objects, not pages. This way i can keep using my nice clean api for the venues properties instead of page->getAttribute('blah') page->getCollectionID() etc. as well as any custom logic that is Venue specific. Ultimately its the same data and the same logic, and I don't want to have to recreate between showing a list of venues or showing a single venue. Theres should be no difference in dealing with a single venue versus a list of them.
So ideally, i'd like a custom PageList to return the venue objects that wrap their pages.
So ideally, i'd like a custom PageList to return the venue objects that wrap their pages.
The PageList class extends the ItemList class. ItemList is fairly generic, and that is where the "pagination" functionality lives (it is referring to "pages of data", not necessarily pages in your site).
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)
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)
So would those getters/setters for attributes actually be on the class I inherit from PageList? That seems odd, because a Venue represents a single venue record, not a list of them. Therefore the venue's getters/setters return just the attributes for the current record.
Oops, yeah you're totally right.
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).
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