Skip to main content

This site requires you to update your browser. Your browsing experience maybe affected by not having the most up to date version.

UncleCheese
20th February 2015

In this tutorial we're going to look at adding forms to the frontend of our website. We'll add a new feature that allows users to post comments on our articles.

Introduction to frontend forms

Level: Beginner

Duration: 17:59

In this lesson:

How SilverStripe handles forms

This lesson is about frontend forms, which is a very important distinction to make, because we've actually been working with forms since very early on in the CMS. Whenever we're editing data in the backend, we're using a form. The main difference is that in the CMS, only a small part of that form is exposed to the model class via getCMSFields().

When you think about how a framework might deal with form creation, you may imagine a simple object that takes an array of form fields and ultimately dumps some HTML via a render() method or something similar, but one of the most dinstinguishing features of forms in SilverStripe is that they are treated a first-class citizens, which is to say they're intelligent objects that work with all the big players in the request cycle.

Creating a simple form

Let's look at a simple form constructor to better understand how this will work. The following method would be placed in any Controller.

    public function ContactForm() {
        $myForm = Form::create(
            $controller,
            'ContactForm',
            FieldList::create(
                TextField::create('YourName','Your name'),
                TextareaField::create('YourComments','Your comments')
            ),
            FieldList::create(
                FormAction::create('sendContactForm','Submit')
            ),
            RequiredFields::create('YourName','YourComments')
        );

        return $myForm;
    }

Let's walk through each argument of the constructor:

  • $controller A form should be generated by, and handled by, a Controller. The benefits to this are impressive, and we'll see some of them in a bit. Because the controller that creates the form usually handles its submission as well, 99% of the time, this first argument is going to be $this.
  • ContactForm A string representing the name of the method that creates the form. Odd, right? It's a bit unconventional to be dealing with class methods as strings, for sure, but this is a really key feature. The form will submit to a URL that calls this method, which means that we get the fully configured Form object to inspect and manipulate when handling submission. We can add error messages, set state, and even manipulate the fields based on the user's submission. Because this argument always describes the name of the function we're in, you can simply use the PHP constant __FUNCTION__ here.
  • A FieldList object containing all of the form fields that accept user input
  • A FieldList object that contains all the form actions, or buttons, that submit a form. Often times you'll only have one action. The first argument of the FormAction constructor is the method on the controller that will be invoked when the form is submitted.
  • A RequiredFields object takes a list of form field names to validate. Validation, as you may know, is infinitely complex. In this case, we're doing very basic validation that simply ensures that the fields contain valid data. Each form field validates itself, so an EmailField will validate differently than a simple TextField.

Handling submission

In the FieldList of actions for our form, we specified a single form action that maps to the method sendContactForm. Let's look at what that method might do. Again, the following code could be in any Controller class.

    public function ContactForm() {
        //...
    }

    public function sendContactForm($data, $form) {
        $name = $data['YourName'];
        $message = $data['YourMessage'];
        if(strlen($message) < 10) {
            $form->addErrorMessage('YourMessage','Your message is too short','bad');
            return $this->redirectBack();
        }

        return $this->redirect('/some/success/url');
    }

Our form handler method is given two parameters:

  • $data contains an array of all the form field names mapped to their values, very similar to what you would get from $_POST.
  • $form is the form object that the ContactForm method gave us. Remember that we had to specify the name of the function in our form constructor? This is why. Now our form handler has full access to that form, which is immeasurably useful.

So that's the really high-level view of how forms work. Now we'll look at implementing a working frontend form in our project.

Adding a form to a template

Let's look at our ArticlePage.ss template again and find the comment form near the bottom of the page. Let's try our best to replicate that form in our controller.

mysite/code/ArticlePage.php

class ArticlePage_Controller extends Page_Controller {

    public function CommentForm() {
        $form = Form::create(
            $this,
            __FUNCTION__,
            FieldList::create(
                TextField::create('Name',''),
                EmailField::create('Email',''),
                TextareaField::create('Comment','')
            ),
            FieldList::create(
                FormAction::create('handleComment','Post Comment')
            ),
            RequiredFields::create('Name','Email','Comment')
        );

        return $form;
    }
}

This is all very similar to what we did in the example. Notice we're leaving the labels for the fields deliberately blank. That's because in the design, they're added with placeholder attributes. Let's make a small update to populate the placeholders of the form fields.

mysite/code/ArticlePage.php

    public function CommentForm() {
        //...
            FieldList::create(
                TextField::create('Name','')
                    ->setAttribute('placeholder','Name*'),
                EmailField::create('Email','')
                    ->setAttribute('placeholder','Email*'),
                TextareaField::create('Comment','')
                    ->setAttribute('placeholder','Comment*')
            ),
        //...
    }

Since this is a public method on the controller, we can add it to the template by calling $CommentForm. Add that variable in place of the static form markup. Refresh the page and have a look.

Looks pretty awful, right? There are still a number of modifications we have to make to our form to keep it in line with the markup the designer provided. Let's make a few more updates.

public function CommentForm() {
    $form = Form::create(
        $this,
        __FUNCTION__,
        FieldList::create(
            TextField::create('Name','')
                ->setAttribute('placeholder','Name*')
                ->addExtraClass('form-control'),
            EmailField::create('Email','')
                ->setAttribute('placeholder','Email*')
                ->addExtraClass('form-control'),
            TextareaField::create('Comment','')
                ->setAttribute('placeholder','Comment*')
                ->addExtraClass('form-control')
        ),
        FieldList::create(
            FormAction::create('handleComment','Post Comment')
                ->setUseButtonTag(true)
                ->addExtraClass('btn btn-default-color btn-lg')
        ),
        RequiredFields::create('Name','Email','Comment')
    );

    $form->addExtraClass('form-style');

    return $form;
}

We use the chainable methods setAttribute and addExtraClass for the main fields and for the form itself, and setUseButtonTag for the form action to force it to render a <button> instead of an <input>. Refresh, and things should look a lot better now.

This is getting a bit verbose. Let's see if we can tighten all that up to add the extra classes and placeholders dynamically.

public function CommentForm() {
    $form = Form::create(
        $this,
        __FUNCTION__,
        FieldList::create(
            TextField::create('Name',''),
            EmailField::create('Email',''),
            TextareaField::create('Comment','')
        ),
        FieldList::create(
            FormAction::create('handleComment','Post Comment')
                ->setUseButtonTag(true)
                ->addExtraClass('btn btn-default-color btn-lg')
        ),
        RequiredFields::create('Name','Email','Comment')
    )
    ->addExtraClass('form-style');

    foreach($form->Fields() as $field) {
        $field->addExtraClass('form-control')
               ->setAttribute('placeholder', $field->getName().'*');            
    }

    return $form;
}

Form methods are chainable, just like form field methods, so we've chained addExtraClass to the constructor. We use the Fields() method of the form to get each field by reference and add the extra class to each one, and create a dynamic placeholder based on the field's name.

This is mostly just a demonstration of form methods.. You probably wouldn't want to do something this aggressive in your project code because it doesn't scale well. Imagine if we had a new field that wasn't required, for instance, and the placeholder shouldn't have an asterisk next to it. Sometimes, it's okay to be verbose and repeat yourself a little bit. You'll be glad you did when you have to make small updates and handle edge cases.

If you're a bit dismayed about having to manually add all of the basic requirements for Bootstrap, rest assured there is a bootstrap-forms module that automatically does most of these updates. We haven't talked about modules yet, so it's a bit out of scope for now, but be aware that this is a bit more complex than it needs to be.

Creating a Comment data model

When a user submits a comment, we want to save it to the database and add it to the page. Before we go any further with forms, we're going to need to do some data modeling to store all that content.

Let's create a simple ArticleComment DataObject. We've seen all this before. mysite/code/ArticleComment.php

class ArticleComment extends DataObject {

    private static $db = array (
        'Name' => 'Varchar',
        'Email' => 'Varchar',
        'Comment' => 'Text'
    );

    private static $has_one = array (
        'ArticlePage' => 'ArticlePage'
    );
}

Notice we have a $has_one back to ArticlePage to set up a $has_many relationship. Let's now follow through with that.

mysite/code/ArticlePage.php

class ArticlePage extends Page {

    //...
    private static $has_many = array (
        'Comments' => 'ArticleComment'
    );
    //...
}

Run dev/build and see that you get a new table.

While we're in here, let's carve up the template and add a loop for all the comments. There are no comments right now, but there will be shortly.

themes/one-ring/templates/Layout/ArticlePage.ss (line 72)

<div class="comments">
    <ul>
        <% loop $Comments %>                        
        <li>
            <img src="$ThemeDir/images/comment-man.jpg" alt="" />
            <div class="comment">                                
                <h3>$Name<small>$Created.Format('j F, Y')</small></h3>
                <p>$Comment</p>
            </div>
        </li>
        <% end_loop %>
    </ul>

Refresh, and the expected result is that no comments appear above the form.

Handling form submission

Now that we have our data models set up, we can start using them in our form handler. Looking at our form method, the name of the handler we've specified is handleComment(). Let's create that method, right below the form creation method.

mysite/code/AriticlePage.php (ArticlePage_Controller)

public function CommentForm() {
    //...
}

public function handleComment($data, $form) {
    $comment = ArticleComment::create();
    $comment->Name = $data['Name'];
    $comment->Email = $data['Email'];
    $comment->Comment = $data['Comment'];
    $comment->ArticlePageID = $this->ID;
    $comment->write();

    $form->sessionMessage('Thanks for your comment!','good');

    return $this->redirectBack();
}

In the handler, we optimistically create the ArticleComment object as a first operation. We can do this because at this point the form has already passed validation, so we know that all of the required fields are in the $data array. You may not always want to do this. You might have some logic that determines otherwise based on the values provided, but let's just keep it simple for now.

Notice that we create that $has_many relation by binding the comment back to the ArticlePage. That's an easy step to forget. Remember that $has_one fields are always suffixed with ID. $this->ID in this case refers to the current page ID. All the properties of a page (Title, Content, ID, etc.) are available in the controller as well as the model thanks to SilverStripe's ModelAsController class.

Let's try submitting the form and see what happens.

Action 'CommentForm' isn't allowed on class ArticlePage_Controller.

Yikes!

What's happening here? We'll get to the error in just a moment, but right now, it's important to understand where we are and what we're looking at.

Take a look at the URL. travel-guides/sample-article-1/CommentForm. The first two segments of that are easily identifiable. It's the URL of our current article page. The last part, CommentForm is what's called a controller action. By default, the URL part that immediately follows the page URL will tell the controller to invoke a method by that name. In this case, we want the CommentForm() method on our controller to execute, because it creates the Form object, which is then passed along to our form submission handler. This, right here, is where most of the magic of forms happens in SilverStripe. They actually submit to a URL that recreates them as they were rendered to the user.

You may have noticed that I casually mentioned an an alarming detail of request handling in SilverStripe -- you can invoke arbitrary methods in the URL.

Exhale. It's not that simple.

In fact, that's precisely why we're seeing this error. We can't just execute arbitrary controller methods from the URL. The method has to be whitelisted using a static variable known as $allowed_actions. Let's do that now.

mysite/code/ArticlePage.php

class ArticlePage_Controller extends Page_Controller {

    private static $allowed_actions = array (
        'CommentForm',
    );

    //...
}

We made a change to a static variable, so we have to run ?flush. Go back to the article page (i.e. remove /CommentForm/ from the URL) and run the flush. We don't want to do this in the middle of a form submission.

Try submitting the form again. You should now see your comment posted.

Binding to a DataObject

Let's take this a step further. We've talked about how forms are first-class citizens in SilverStripe. Part of that is being model-aware. Looking at our handler method, we see that all the form parameters are named exactly the same as the ArticleComment database fields. This is ideal, because it means we can take advantage of a massive time-saving method of the form class known as saveInto().

Let's modify our function to call saveInto() instead of manually assigning all the form values.

myite/code/ArticlePage.php (ArticlePage_Controller)

    public function handleComment($data, $form) {
        $comment = ArticleComment::create();
        $comment->ArticlePageID = $this->ID;
        $form->saveInto($comment);
        $comment->write();

        $form->sessionMessage('Thanks for your comment','good');

        return $this->redirectBack();
    }

Notice that we still have to manually assign the ArticlePageID field, as that is not present in the form data. We could have passed it via a hidden input, which would eliminate that line of code.

What's great about this method is that it can actually respond to the needs of a specific model. If our ArticleComment object had a method called saveComment() or saveName(), it could save the form data in its own specific way. So it may look like a shotgun approach, but it can actually be pretty granular if you want it to be.

Dealing with validation

Our form is accepting submissions and working as expected, so let's now add a bit of validation. We're already using RequiredFields, which is our primary sentinel against bad data, but what if we want to add some custom logic that goes beyond simple sanity checks?

Adding custom validation logic to the handler

If the logic were really complicated, we could write our own validator, which we'll cover in the future, but for simple validation, it's fine to do all of this in your form handler method. Let's run a check to make sure the user's comment has not already been added. You might think of this as really basic spam protection.

mysite/code/AritclePage.php (ArticlePage_Controller)

    public function handleComment($data, $form) {
        $existing = $this->Comments()->filter(array(
            'Comment' => $data['Comment']
        ));
        if($existing->exists() && strlen($data['Comment']) > 20) {
            $form->sessionMessage('That comment already exists! Spammer!','bad');

            return $this->redirectBack();
        }        

        // ...
    }

Before creating the Comment object, we first inspect the $data array to see if everything looks right. We look for a comment on this page specifically that contains the same content, and if so, we add a message to the top of the form. The value 'bad' as the second argument gives it an appropriate CSS class. 'good' is the other option here.

To filter out false positives, we make sure the comment is at least 20 characters long. It's plausible that multiple readers might comment "Nice article" or "Good work" and we don't want to punish them.

Again, the lesson here is not about spam protection, but just how to handle form validation. Don't accept this as gospel on how to secure your blog comments.

Try submitting the form again with an existing comment, and you'll see that we generate an error message.

Preserving state

There's one usability problem here, and perhaps we shouldn't worry about it too much since we're not particularly motivated to be nice to spammers, but for the sake of teaching the concept, it would be nice if the form saved its invalid state, so that the user doesn't have to repopulate an empty form. For this, the convention is to use Session state.

mysite/code/ArticlePage.php (ArticlePage_Controller)

    public function handleComment($data, $form) {
        Session::set("FormData.{$form->getName()}.data", $data);
        //...
        $comment->write();

        Session::clear("FormData.{$form->getName()}.data");
        $form->sessionMessage('Thanks for your comment!','good');

        return $this->redirectBack();
    }

We create a SKU using the form name to use as a sesssion token, and store the $data array there. If everything checks out, we clear it, so that the form renders clean on the next page load. If not, we're going to want the form to render the session data.

mysite/code/ArticlePage.php (ArticlePage_Controller)

    public function CommentForm() {
        //...

        foreach($form->Fields() as $field) {
            $field->addExtraClass('form-control')
                  ->setAttribute('placeholder', $field->getName().'*');            
        }

        $data = Session::get("FormData.{$form->getName()}.data");

        return $data ? $form->loadDataFrom($data) : $form;
    }

Using the ternary operator, we look to see if $data exists. If it does, return the form with the data loaded into it using loadDataFrom. If not, just return the form as is. Remember, most form methods all return the Form object, so it's okay to return the result of loadDataFrom() here.

Test out your form and see that it now preserves its state after failed validation.

Questions and Feedback

Hello,

thanks for an great tutorial.

I see that one-ring template is using Bootstrap. Could you please give me an example how can I use bootstrap-datepicker in FrontEnd forms instead of standard jQuery datepicker?

Thanks a lot!

by Peter Repan at 10:52am, 14 March 2015

Author

Hi, Peter,

So the first thing I would check is if anyone has built any modules that offer you a bootstrap-datepicker field. First glance, I wasn't able to find any, so you'll probably have to roll your own.

Start with a DateField, and just don't use the ->setConfig('showcalendar', true) on it, so that it doesn't load the jQuery plugin, and instead just gives you a text field. Then invoke ->addExtraClass('my-datepicker-class'), or better yet ->setAttribute('data-datepicker', true). Then, load your script with Requirements::javascript() and Requirements::css() and have your script look for data-datepicker inputs on load, and attach the plugin.

For an extra 10 points, you could put this into a module, as a subclass of DateField. Here would be the rough idea:

class BootstrapDateField extends DateField {
  public function FieldHolder($attributes = array ()) {
    Requirements::css(....); // datepicker css
    Requirements::javascript(...); // datepicker js
    Requirements::customScript(...) // JS that attaches the datepicker to [data-datepicker]
    return parent::FieldHolder($attributes);
  }
}

For an extra 20 points, you could allow the user to customise the datepicker with methods that add more data- attributes to the field, and have your script check the values of those on load.

Does that make sense?

by UncleCheese at 01:30pm, 14 March 2015

Hi UncleCheese,

I only added reference to bootstrap-datepicker.css in Page.php and set

DateField::create('Date')->setAttribute('data-datepicker', true)

and it works :). The script for attaching datepicker is out-of-the-box in theme and included on every page.

Thank you very much for an example and bonus challenges :)

By the way, in a case that I'm using on page more (let's say three) "BootstrapDateField(s)" and every field is calling the same css and js, the framework ensures that every css/script will be loaded only once, or three times?

by Peter Repan at 02:10am, 15 March 2015

Author

Hi, Peter,

The Requirements methods are unique-protected against the pathname to the file, so you're fine. It gets a bit tricky with things like jQuery, which can come from a number of different sources. For that, you can pass a unique identifier as the second argument:

Requirements::javascript('path/to/jquery/jquery.js','jQuery');

Then, when another module tries this:

Requirements::javascript('path/to/another/jquery/jquery.js', 'jQuery');

It won't load. Nice feature, but it should be noted that it requires both parties to use it, and that is seldom the case. Frontend dependency management is a big can of worms in just about every framework.

by UncleCheese at 02:00pm, 16 March 2015

Can't get the form to render correctly. Post Comment button covers the center comments text box. Also how to limit comment length and add bad word filter?

by john at 07:08pm, 24 March 2015

Author

Hi, John,

Since the comment is in a textarea, you can't really limit it client side without some Javascript, but you can limit the length server side by either running substr() against the comment, or, you could check strlen() on the comment and throw a validation error if it exceeds a certain length.

There's no reliable way to filter bad words, but some folks have tried. (http://banbuilder.com)

by UncleCheese at 03:30pm, 1 April 2015

Uncle Cheese,

First off, a million thank you's for all you have done and continue to do for the community!

My question is, in this video tutorial series it seems like you're setting up a multi-input (faceted search) somewhere. I was wondering if you're going to cover this in a later tutorial? I followed along with this tutorial and it was great, but I'm trying to create a multi-input search and I'm understanding the most of what you're doing here. I've created a search form that has multiple input fields, I can pass that data to a form handler, but I'm stuck on returning the results from the handler. In this example you're writing to the DB and returning a message in session variable for success and failure. How would I go about accomplishing returning the Datalist I'm receiving?

Jamie

by Jamie T at 04:25am, 30 March 2015

Author

Hi, Jamie,

Great question! We'll be covering search in the next tutorial. Search forms work differently than submission forms, because typically they take you to a stateful URL, i.e. using GET parameters. So your form handler is essentially just a redirect to a URL that can handle those URL params. That way, no one has to submit the form to get the results, which makes it possible to share a link.

Stay tuned. We'll be lighting up that form next.

by UncleCheese at 03:24pm, 1 April 2015

It would be nice to add here how you can send an email for example telling the site owner that a new comment inside the handleComment function. it seems to be easy enough, but not obvious for newcomers

by Francisco at 07:43am, 19 May 2015

make validation for email

by jack at 12:45am, 22 May 2015

Hi,

Before I ask my stupid question, I will just elaborate my project, I have a page called BranchPage which i can use multiple times and rename it to whatever branch they may have (e.g. hawera, hamilton, christchurch, auckland, so on). Now, I have got multiple articles that should display accordingly into each branches. As you say on your tutorials, do not repeat so I used an include as <% include articles %>.

My question is, is it possible to manipulate these articles to be able to display as per branch using includes?

My initial plan was

  1. Gathering the article and create a dropdown or checkboxes on the cms
  2. When I'm on my branch, I can choose few articles on my page to display as featured article

Thanks!

by Jem at 03:28pm, 11 July 2015

To update my question,

I have successfully record the article that has been chosen on the branchpage checkboxes as featured articles. However, I have only record the IDs. I want to display the whole article that has been chosen.

public function DisplayArticle($num=3) {
    $article = $this->Article; //this is the saved article on this branchpage that is from the articlepage
    if($article = ArticlePage::get()->$this->ID) {
        $action = ArticlePage::get()->filter('ParentID', $article->ID)->sort('Date DESC')->limit($num) : false;
    } 
    return $action
}

this is how i saved the article

private static $db = array (
        'Article' => 'Varchar',
);

public function getCMSFields() {
        $fields = parent::getCMSFields();
        $field = CheckboxSetField::create('Article', 'Articles to show on this page', ArticlePage::get()->map('ID', 'Title'));
        $fields->addFieldToTab('Root.Article', $field);

     return $fields;
}

by Jem at 08:15pm, 14 July 2015

Author

Hi, Jem,

When you're pasting code, be sure to use the syntax highlighting provided by Github flavoured markdown. More here: https://help.github.com/articles/github-flavored-markdown/#syntax-highlighting

I think you're working a bit too hard. To create a relation between two pages, you want to use a many_many for your checkboxes.

private static $many_many = array (
        'Articles' => 'ArticlePage',
);

public function getCMSFields() {
        $fields = parent::getCMSFields();
        $field = CheckboxSetField::create('Articles', 'Articles to show on this page', ArticlePage::get()->map('ID', 'Title'));
        $fields->addFieldToTab('Root.Article', $field);

     return $fields;
}

Then, on your template:

<% loop $Articles %>
<h3><a href="$Link">$Title</a></h3>
<% end_loop %>

Your DisplayArticle() function is not necessary as far as I can tell.

If you ever wanted to know which branch pages an article was associated with, you could create a belongs_many_many back to BranchPage.

by UncleCheese at 05:15pm, 16 July 2015

Wow, this works like a charm. That's how easy it is? Spent hours figuring this out.

Thanks Unclecheese you're the man.

by Jem at 06:47pm, 16 July 2015

Author

Thanks, Jem. I covered this all in lesson 10, if you want more information on it.

by UncleCheese at 10:23am, 17 July 2015

I have a problem with how this page renders after we add the addextaclass to the fields there are 2 boxes per field and they only fill haft the area available the comment box covers a small box under nenth it and the button is over the top ?? I have check the php and ss files to make sure they match the ones in the folders and they seem to be the same??

by Ken hoffman at 02:45pm, 3 August 2015

Hi,

Do you have any tips on how you would implement a reply button on each Comment?

by Michelle at 05:46am, 10 December 2015

Hi, Michelle,

Great question. We actually are using that type of feature right here in the comments. The way I implemented it here was probably not ideal, but I'll describe how I did it.

First, set up a self-referential relationship for the comments.

class Comment extends DataObject {
    private static $has_one = ['Parent' => 'Comment'];

    private static $has_many = ['Replies' => 'Comment'];
}

In the function that creates the comment form, allow it to accept a $replyToID.

public function CommentForm($replyToID) {
    // add fields
    $fields->push(HiddenField::create('ParentID','', $replyToID);

    if($replyToID && is_numeric($replyToID)) {
        // Manipulate form 
    }
}

Now, when looping through the comments, pass the comment ID to the form.

<% loop $Comments %>
<p>$Comment</p>
<h3>Reply to this comment</h3>
$Top.CommentForm($ID)
<% end_loop %>

Make sense?

by UncleCheese at 11:00am, 19 January 2016

Thanks, David. That makes sense. I'll look into getting that fixed.

by UncleCheese at 11:53am, 19 January 2016

Hi

In the written tutorial you write: "The value 'bad' as the second argument gives it an appropriate CSS class." How do I get and use that value from sessionMessage?

by Kim K at 10:16pm, 21 February 2016

Hi, Kim,

If you're rendering the entire form in a single template variable, e.g. $Form, the error messages should render automatically. If you're doing some custom rendering, you'll need to get them explicitly, like so:

<% with $Form %>
<form $FormAttributes>
    <% if $Message %>
        <p class="message $MessageType">$Message</p>
    <% end_if %>
</form>
<% end_with %>

by UncleCheese at 03:56pm, 9 March 2016

Oopsie....it turns out that Created already exists in ArticleComment and so all that is needed is to put $Created in your templates....sorry! The above works but is not needed...

by SpiritLevel at 02:54pm, 29 February 2016

Hello, i have a problem with site rendering. After i've added addExtraClass('form-control') to field it displays 2 fields on the site, one field over another. I don't know how to make it right and display just one field. With button it works correctly. Could you please give me some advice? Thanks.

by Martin at 09:25pm, 22 April 2016

Stuck on something? Have something to share? Don't be shy!

Keep learning!

Working with data relationships - $many_many

In this tutorial, we'll add categories to our articles using another type of plural data relationship known as $many_many.

Working with data relationships - $has_many

In this lesson, we're going to dig a bit deeper into relational data by introducing some plural relationships with $has_many.

Introduction to the ORM

In this lesson, we’ll use the backbone of SilverStripe Framework -- the Object Relational Model (ORM) to syndicate content to the home page of our...