Apologies in advance for the long post...

I've been tasked with building a User-generated content site with ExpressionEngine. I had a rough idea of how I was going to proceed but - as I have progressed - it feels like I'm having to "bend the rules" more and more to achieve my intentions, which usually means that I'm doing something wrong :( Any feedback would therefore be greatly appreciated.

I'll first start by describing the intended architecture: The site consists of an open membership system which allows front-end registration, login, logout and profile capabilities. There are then a number of channels such as "jobs", "events", "workshops" - each following a similar structure of listing/view page - to which members can contribute from the front end. Once they have submitted an entry to one of these channels, they should be able to Edit or Delete the entry at a later date, or mark it as closed (filled).

So far, I've been working along the following lines of implementation: I'm using Structure to manage the pages and Zoo Visitor to handle the membership system (chosen as there were a number of checkbox fields that I needed to add to the registration form). This side of things is all working fine.

When a new job is added, I intend to have it use the id and job title to form the URL string, so that if another job already exists with the same title I don't end up with things like "web-developer-2". I haven't actually done this yet, but I imagine that I can just populate a hidden field on the add form.

For any one of these given areas (eg. /jobs/ ), I intended to have the following URL structure:

/jobs/                           jobs listing page
/jobs/<id>-<job-title>/          job single page
/jobs/add/                       add job page
/jobs/edit/<id>-<job-title>/     edit job page
/jobs/delete/<id>-<job-title>/   delete job page
/jobs/success/                   success page for add/edit/delete job
/jobs/filter/today/              jobs posted today          
/jobs/filter/this-week/          jobs posted in last 7 days
/jobs/filter/2013/oct/           jobs posted in october 2013
...                              (previous 12 months)
/jobs/filter/2012/nov/           jobs posted in november 2012
/jobs/filter/archive/            (including those with a status of 'closed')

Obviously on top of all this there is pagination which takes the form of p10, p20 etc (10 to a page)

I've been using Freebie to ignore anything after the /jobs/ segment, and then I've been using the jobs/index template a kind of controller:

{!-- Archive pages --}
{if freebie_3 == "archive"}

{!-- CRUD pages --}
{if:elseif freebie_3 == "add"}
{if:elseif freebie_3 == "edit"}
{if:elseif freebie_3 == "delete"}
{if:elseif freebie_3 == "success"}

{!-- Listing / Single page --}        

In the view template, I've the got:

{exp:channel:entries channel="jobs" required_entry="yes" limit="1" url_title="{freebie_last}" dynamic="no"}
    {if no_results}
        {!-- jobs listing page --}
    {!-- jobs detail page --}

In the add template I've got:

{if logged_in}
    <p>Sorry, you must be logged in to use this feature.</p>

In the edit and delete templates, I've got:

{if logged_in}
    {exp:channel:entries channel="jobs" required_entry="yes" limit="1" url_title="{freebie_last}" dynamic="no"}
        {if no_results}
        {!-- Edit code here --}
    <p>Sorry, you must be logged in to use this feature.</p>

The add/edit/delete templates will use Channel:form (formerly Safecracker) for the allow the adding, editing and deleting of entries. (I haven't written the code which detects if the form has been submitted and actually does the edit/delete action yet).

I guess I'll need to wrap the latter two functions in...

{if author_id == logged_in_member_id}

...to make sure that people can only edit/delete their own entries.

So I guess my question is...

Is there a better method - module or otherwise - of handling CRUD functionality from the front end, other than that which I have described?

It had dawned on me that a better approach might be to write a custom module that interacts directly with the database (rather than using the channels at all) as this might keep the model/controller cleaner and more separated from the view; but I haven't done this before so I'm hesitant to go down this road as I'm sure it will throw up a whole new set of hurdles...

Any advice is greatly appreciated :)

  • 1
    "I didn't have time to write a short letter, so I wrote a long one instead" - Mark Twain Oct 17, 2013 at 21:09

3 Answers 3


Your question is not very specific, so we can't give you specific answers... however EE can handle all of this stuff, but it does take some planning. Here are a few comments in no particular order that may help...

Firstly I'd recommend ditching Structure, as you'll be fighting it at every step for control of your URLs and you could also find that it hinders future growth. (Imagine for example, expanding the site to include multiple client sub-domains or something fun like that). This will save you the awkwardness of building 'controller'-like routing templates and instead use native URL handling. This will be especially handy for filtering by date, which EE can do natively if you just pass along the right segments.

Building a custom module would be an excellent clean option and even if it's your first you probably won't regret it as you'll have complete control, and it's a great learning experience as well as good fun. Bear in mind that you can also take a half-way approach, which would be to use Safecracker/Channel Form etc, but build your own add-on that hooks into Safecracker's routines and does extra stuff before/after submission: http://ellislab.com/expressionengine/user-guide/development/extension_hooks/module/channel_form/index.html

Another couple of points: Safecraker/Channel Form has an author_only="yes" parameter to do what you describe for editing entries.

Safecracker has no native capability for deleting entries. It's straight-forward to build this functionality yourself with the help one of the existing deletion add-ons (http://devot-ee.com/search/results?keywords=delete&addon_version_support=ee2), but naturally you have to be extremely conscious of security, making sure that a logged-in member can only delete their own entries and locking down things like the channel from which they can delete etc. This is where your own custom add-on can help tighten things up a bit.

Ben's Open-API mentioned previously looks like it will become a brilliant alternative to Safecracker/Channel Form, however at this point in time (I think) it lacks support for 3rd party fieldtypes, so that may become an issue.

One other little bit of advice is that instead of using loads of repetitive {if logged_in}...{if:else} sections, consider instead using a single redirection snippet that can be placed at the top of any non-public templates to redirect logged-out users to a generic 'permission-denied' template (or a standard login template). This avoids the 'advanced conditionals trap' and also allows you to easily alter the logic of what exactly constitutes a 'logged-in' user (eg, what happens when your site grows and you start accepting membership from job applicants as well as job advertisers etc).

Good luck - this will be a fun project for you. I've recently completed a sporting web app where the content is entirely user-generated, and EE coped admirably - though with a lot of help from a custom add-on that basically served to tie together Matrix, Playa, Profile:Edit, Safecracker, Postmaster and Cartthrob into a cohesive and secure whole.

  • Open API may be able to handle what you need however the tricky part will be handling member permissions. It does support third party fieldtypes, provided they store their data in the exp_channel_data table.
    – Ben Croker
    Oct 3, 2013 at 7:24
  • This is an excellent answer, thank you James. I think I agree that Structure is causing more troubles than it's worth at the moment. The idea of a custom module is appealing as it seems like the "correct" way to do it and far less hacky than what I've been doing so far. I think that this, along with a more coherent template structure (courtesy of dadonbike) will be the way forward.
    – Joe Czucha
    Oct 4, 2013 at 8:34

Ben Croker of PutsYourLightON has been working on an API specifically designed for CRUD functionality. You should take a look at it here... http://www.putyourlightson.net/open-api

And here is the repo for it... https://github.com/putyourlightson/open-api

I have messed with this some and it has some great potential.

  • Wow this looks great, thanks Mike. I'll take a closer look at it in the morning.
    – Joe Czucha
    Oct 2, 2013 at 17:32

Here's a different page/embed approach to ponder, it may make maintenace much easier.

Think inside out and consider using partials. about partials

Heres our layout. Template = layout/.default

<!DOCTYPE html>
<html lang="en">
        <title>{exp:stash:head_title} - LocalYak</title>
        <div id="main_bkgd">
            <div id="main" class="device_{ress} view_{view}">
                <div id="content" class="{if ress > 1}rounded{/if} {exp:stash:get name='content_class'}">

And heres a (page) template that calls and uses that layout.

{exp:stash:set name='head_title'}Compare{/exp:stash:set}
{exp:stash:set name='head_links'}
    <link rel='stylesheet' type='text/css' media='screen' href="{stylesheet='advertise/.css_advertise'}" />
    <link rel='stylesheet' type='text/css' media='screen' href="{stylesheet='pages/.css_pages'}" />
    <meta name="description" content="Compare advertising packages" />
{exp:stash:set name='content_class'}tab-advertise outline{/exp:stash:set}
{exp:stash:set name='content'}
    <div class="page_select body_wrap">
        <h1>Compare packages</h1>
        [put your page content here.... or if you are forced to like we did on this occassion, create an extra embed]{embed="advertise/.select_type"}

And if you need to show different content for the page, simply wrap using simple conditional like this

{if segment_3 == ""}
    {exp:stash:set name='content_class'}category splash{/exp:stash:set}
    {exp:stash:set name='content'}some content or tags{/exp:stash:set}
{if segment_3 != ""}
    {exp:stash:set name='content_class'}category splash{/exp:stash:set}
    {exp:stash:set name='content'}some different content or tags{/exp:stash:set}

Our site is mostly built on that one single default layout, however we do use a total of seven layouts.

And here an example of a safecracker page (channel form)

{embed="layout/.mybusiness" page_id="latest_news"}
{exp:stash:set name='head_title'}Edit latest news{/exp:stash:set}
{exp:stash:set name='head_links'}
    <link rel="stylesheet" type="text/css" media="screen" href="{stylesheet='_ee_saef_css'}"/>
{exp:stash:set name='body_wrap_class'}edit edit_news{/exp:stash:set}
{exp:stash:set name='content'}  
        channel = "browse" 
        url_title = "{segment_3}"
        return = "mybusiness/view/URL_TITLE/{segment_4}/latest_news"
        require_entry = "yes"
        include_jquery = "no"
        {if global:sc_https_on}
            secure_action = "yes"
            secure_return = "yes"
        <div class="rounded border box">
            <label class="first" for="latest_news">Enter your latest community news.</label>
                <div class="btn_wrap"><a href="{path='mybusiness'}/view/{segment_3}/{segment_4}/latest_news" class="btn green" rel="nofollow">cancel</a></div>
                <div class="btn_wrap"><button type="submit" name="submit_button" class="btn green"><span>save</span></button></div>

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.