Today we are going to expand our knowledge of PHP frameworks and CMSs. While I usually write about Symfony, today we are going to talk about SilverStripe. Compared to the likes of Laravel and Symfony, this is a small framework when we count stars on GitHub. The silverstripe-framework repository has 700+ stars. When compared as a CMS against Bolt (which I've written about previously), SilverStripe sees a bit more of stardom. Bolt sits on 300+ stars for the current iteration.
So what is SilverStripe? It's main purpose of existence is its CMS component. The framework part of it has some likeness to Laravel, but appears in a first impression to exist to be a part of a content management system (perhaps I may explore just the framework side of it later on). The SilverStripe CMS markets itself as "the intuitive content management system and flexible framework loved by editors and developers alike". It's big words to live up to, so let's see how it fairs as we set out to build a Book Review platform (because the Dune movie has gotten me to read the Frank Herbert novel and I like to record that experience).
We will be building a fairly basic Book Review platform. In this first iteration we will be doing most things manually. Entering the books and authors will be all manual, and we will only have the one user (the default admin user). In a follow up to this article we will be adding a registering page, and using the power of Symfony's HttpClient to make requests to the Google Books API - so we won't have to do that manually anymore.
If you want the code rather than follow along, this project can be found at https://github.com/andersbjorkland/devto-book-reviewers/tree/basic.
The ingredients of a Book Review Platform
The features we are going to want is:
- Reviewing books
- Add book
- Add author
- Add review
- Viewing reviews
So we are going to need a few things for this project:
- PHP 8.0 (you may run with PHP 7.3> and it will mostly work)
- MariaDB 10 (Docker image is compatible with arm-processors)
- SilverStripe 4.*
- And some Composer dependencies we are going to attach as we move along
Oh, there are also a few PHP extensions we need.
Let's build this!
Installing Silverstripe
Run the following with Composer in your favourite terminal:
composer create-project silverstripe/installer book-reviewers
If you have PHP8 this command may fail. It's a dev-dependency in the regular recipe causing this. What you will do then is to modify composer.json by removing the following lines:
"require-dev": { "sminnee/phpunit": "^5.7", "sminnee/phpunit-mock-objects": "^3.4.5" },
We will not be using PHPUnit or Mock Objects for this project, so it's safe to disregard these dependencies this time.
You may now go ahead and install SilverStripe. From within the project directory, run the following:composer install
After the installation is done, there are a few configurations needed. We are going to set up a database and an admin-user. I'll be using docker-compose for the database. First off, let's create a .env
-file in the project-root:
# DB credentials
SS_DATABASE_CLASS="MySQLDatabase"
SS_DATABASE_SERVER="localhost" # try 127.0.0.1 instead if you get connection error
SS_DATABASE_USERNAME="user"
SS_DATABASE_PASSWORD="password"
SS_DATABASE_NAME="silverstripe"
# Admin user
SS_DEFAULT_ADMIN_USERNAME="admin"
SS_DEFAULT_ADMIN_PASSWORD="password"
# WARNING: in a live environment, change this to "live" instead of dev
SS_ENVIRONMENT_TYPE="dev"
Setting up docker-compose for the database
This part of the project requires Docker Desktop - available for Mac and Windows alike. If you're on Linux there exists the Docker Engine for it, and Docker Compose.
Now, let's create a docker-compose.yml
file, also in the project root:
version: '3.4'
services:
db:
image: mariadb:10
env_file:
- .env
ports:
- 3306:3306
environment:
MYSQL_ROOT_PASSWORD: ${SS_DATABASE_PASSWORD}
MYSQL_PASSWORD: ${SS_DATABASE_PASSWORD}
MYSQL_USER: ${SS_DATABASE_USERNAME}
MYSQL_DATABASE: ${SS_DATABASE_NAME}
MYSQL_INITDB_SKIP_TZINFO: 0
The docker-compose will use some of the environment variables from the .env file to configure the database.
With docker-compose.yml configured, we can launch it from the terminal with:
docker-compose up -d
Matter of namespace
By default, SilverStripe uses local namespaces. My preference is PSR-4, so we will add the following to the composer.json
file in the project root (right after the "extra"
-key is what I did):
"autoload": {
"psr-4": {
"App\\": "app/src/"
}
},
Launching SilverStripe
Now that the database is up and running, we can launch SilverStripe. I like to launch all my virtual servers with the Symfony CLI tool. An alternative to this is launching with the built in virtual server that comes with PHP. Whichever way you launch it, the first time loading the SilverStripe site will take a while as it builds the data schema and cache. You may need to configure php.ini setting max_execution_time
to a higher value
Symfony CLI Server
To launch the Symfony CLI server, in the project root run the following:
symfony serve -d
By default, this will launch the server on port 8000, so you can access it at https://127.0.0.1:8000/.
PHP Built-in Server
To launch the built-in server, move into the public
folder and run the following:
php -S localhost:8000
You may now visit the site at http://localhost:8000/.
Modeling the Book Review Platform
Models represent the data in the database, in an object oriented way. In SilverStripe, models are similar to models in Laravel. This means that we are not having entities and repositories as we would with a Symfony application. Instead, models define both the data and a way to interact with the data. Our models will extend a DataObject
-class which will provide us with the basic functionality.
As we are going to build a Book Review platform, we will need to model the following:
-
Book
- Title
- ISBN
- Description
- Author (Many-to-Many relationship)
- Review (One-to-Many relationship)
-
Author
- GivenName
- AdditionalName
- FamilyName
- Book (Many-to-Many relationship)
-
Review
- Rating
- Review
- Member (Many-to-One relationship) - a member is how users are designated in SilverStripe
- Book (One-to-Many relationship)
A few things are going to be handled automatically for us, such as ID-generation and timestamps.
We are using Object relational mapping (ORM) to map the data to the database. This means that we are using the SilverStripe DataObjects to represent the data, but underneath the hood, we are using a database to store the data in a couple of tables. When there are relationships of different kinds, this means that a table will store a key that links to another table. So, for example, a book can have many authors, and an author can have many books. These relationships are stored in a table with keys for each different author and book.
- Many-to-Many; each object can have many other objects, and each other object can have many other objects. Our case is a book can have many authors, and an author in turn can have many other books. Many-to-many on Wikipedia
- Many-to-One; each object can have only one other object. The other object have many of this object. Our case is a review can have only one user, and a user can have many reviews. This is the reversed side of One-to-many, read about it on Wikipedia
- One-to-Many; each object can have many other objects, and each other object can have only one other object. Our case is a book can have many reviews, and a review can have only one book. One-to-many on Wikipedia PS. I didn't mean for this to becomea lecture in relational databases, it's just a happy coincidence! 💡
Let's go Modeling!
Let's start by creating a Book model. We will extend the DataObject
-class, and add properties that we want to store in the database.
Book Model
Create the file ./app/src/Model/Book.php
which has the following content:
<?php
namespace App\Model;
use SilverStripe\ORM\DataObject;
class Book extends DataObject
{
private static $table_name = "Book";
private static $db = [
'Title' => 'Varchar(255)',
'ISBN' => 'Varchar(255)',
'Description' => 'Text',
];
private static $has_many = [
'Reviews' => Review::class,
];
private static $many_many = [
'Authors' => Author::class
];
}
Let's describe what we've got here. First, we specify what we want the table to be called in the database that will store our books.
static $table_name = "Book";
Then, we specify the properties that we want to store in this database table:
private static $db = [
'Title' => 'Varchar(255)',
'ISBN' => 'Varchar(255)',
'Description' => 'Text',
];
What's left to describe is the relationships. We have a many-to-many relationship with authors, and a one-to-many relationship with reviews. We therefore have specify that a book can have many reviews with the $has_many
-property. We also have a many-to-many relationship with authors, which we will specify with the $many_many
-property.
Author Model
The Author model will be our way of describing an author, as well as its relation to books. We will se the opposite side of a many-to-many relation, and a couple of new nifty features of SilverStripe. Create the file ./app/src/Model/Author.php
which has the following content:
<?php
namespace App\Model;
use SilverStripe\ORM\DataObject;
class Author extends DataObject
{
private static $table_name = "Author";
private static $db = [
'GivenName' => 'Varchar(255)',
'AdditionalName' => 'Varchar(255)',
'FamilyName' => 'Varchar(255)',
];
private static $belongs_many_many = [
'Books' => Book::class
];
private static $summary_fields = [
'GivenName',
'FamilyName',
];
public function validate()
{
$result = parent::validate();
if (!$this->GivenName) {
$result->addError('GivenName is required');
}
if (!$this->FamilyName) {
$result->addError('FamilyName is required');
}
return $result;
}
/**
* @return string
*/
public function getTitle()
{
$givenName = "";
$familyName = "";
$schema = static::getSchema();
if ($schema->fieldSpec($this, 'GivenName')) {
$givenName = $this->getField('GivenName');
}
if ($schema->fieldSpec($this, 'FamilyName')) {
$familyName = $this->getField('FamilyName');
}
if ($givenName && $familyName) {
return $givenName . ' ' . $familyName;
}
return parent::getTitle();
}
}
Again we see private static $db
where we define the author's properties. We also see that we have a many-to-many relationship with books, but this time we define it with private static $belongs_many_many
. The "belongs" here signifies that we probably will be interacting with books more often than authors, such as we create a book and attach a particular author to it.
Then we have this:
private static $summary_fields = [
'GivenName',
'FamilyName',
];
The $summary_fields
-property is used to define ehich fields are searchable as well as which will be used when presenting this model in a list on the CMS. We have specified that we want to show the author's given name and family name.
Next up we are using a neat function that controls what values are permissable or not:
public function validate()
{
$result = parent::validate();
if (!$this->GivenName) {
$result->addError('GivenName is required');
}
if (!$this->FamilyName) {
$result->addError('FamilyName is required');
}
return $result;
}
In the validate()
function we are checking that we get any truthy values for GivenName
and FamilyName
. We allow AdditionalName
to be empty.
Another function we get to see in our Author model is getTitle()
. This function is used to get a simple and readable name for models in the CMS. By default, this function would try to return a 'Name' or 'Title' field, failing that, it will return the ID of the model. As we have neither 'Name' or 'Title' we are instructing the CMS to use 'GivenName' and 'FamilyName' instead.
This is all for the Author model, now let's move on to the Review model.
Review Model
We have one model left to describe. It's our Review model. We will again specify a table-name and fields for our model. This time we will also see our first opposite-side of a many-to-one relationship. There are also some nice features we are using, so let's see what we get with this. Create the file ./app/src/Model/Review.php
with the following content:
<?php
namespace App\Model;
use SilverStripe\ORM\DataObject;
use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
class Review extends DataObject
{
private static $table_name = "Review";
private static $db = [
'Title' => 'Varchar',
'Rating' => 'Int',
'Review' => 'Text'
];
private static $has_one = [
'Book' => Book::class,
'Member' => Member::class
];
private static $owns = [
'Book',
'Member'
];
private static $summary_fields = [
'Title',
'Book.Title'
];
public function populateDefaults()
{
$this->Member = Security::getCurrentUser();
parent::populateDefaults();
}
public function validate()
{
$result = parent::validate();
if ($this->Rating < 1 || $this->Rating > 5) {
$result->addError('Rating must be between 1 and 5');
}
if ($this->Member != Security::getCurrentUser()) {
$result->addError('Only you may be the reviewer of a book that YOU review.');
}
return $result;
}
}
We again see the use of $table_name
and $db
to define the fields of our model. What's new for us in this model is $has_one
and $owns
:
private static $has_one = [
'Book' => Book::class,
'Member' => Member::class
];
private static $owns = [
'Book',
'Member'
];
$has_one
indicates that each Review has either a one-to-one or many-to-one relationship with each entry into its array. As we have specified in the Book model that it has a one-to-many relationship with the Review model, we know that there can be many reviews for each book. We also have a one-to-one relationship with the Member model, which means that each review is associated with a particular member. We have not specified for the Member model that it has a one-to-many relationship with the Review model, and we don't have to! We are using $owns
to specify that the Review model owns the relationship with the Member model. The same doesn't go for the Book model, as we have specified that it has a one-to-many relationship with the Review model, we have to have the reflected side of that relationship represented there. The reason we have 'Book'
in the $owns
array is because we want to be able to control the relationship with these from within the Review model.
Next up we see the use of a new function:
public function populateDefaults()
{
$this->Member = Security::getCurrentUser();
parent::populateDefaults();
}
This function is used to populate the default values for the Review model. We are accessing the Member
field and set it to the current logged in member. We also have to call the parent function to ensure that the default values are populated correctly.
Next up we again see the use of the validate()
function where we check that the Rating
is between 1 and 5, and that the Member
is the current logged in member (if someone were to try anything different than the default).
We have now set up all the models we need: Book, Author and Review. What we are going to do next is to setup a way to allow users to create new books and authors. We will do this by creating a new tab in the CMS, called "Review", where we can administrate authors, books and reviews. The way to do that is creating a class that will extend SilverStripe's ModelAdmin
class. We will see this next!
Administrating our models with ModelAdmin
What we are going to do now is creating an interface for our users to create all that is needed for reviewing a book. That is, we need the user to be able to create authors and books, and then be able to create reviews for those books. So here's what we will do:
- Create a new folder in the
./app/src/
directory calledAdmin
. - Create a new file in the
./app/src/Admin/
directory calledReviewAdmin.php
.
The file ./app/src/Admin/ReviewAdmin.php
will look like this:
<?php
namespace App\Admin;
use App\Model\Author;
use App\Model\Book;
use App\Model\Review;
use SilverStripe\Admin\ModelAdmin;
class ReviewAdmin extends ModelAdmin
{
private static $managed_models = [
Author::class,
Book::class,
Review::class,
];
private static $url_segment = 'reviews';
private static $menu_title = 'Reviews';
private static $menu_icon_class = 'font-icon-book';
}
Let's review what we are doing here. We are creating a new class called ReviewAdmin
that extends the ModelAdmin
class. We are telling the CMS that this class will manage the Author
, Book
and Review
models. We are also telling the CMS that this class will be available in the CMS under the tab called "Reviews", this tab will use an icon called font-icon-book
. This is quite powerful, as this is all we need to be able to start reviewing books.
We have now set up our CMS for reviewing books. Let's update it to handle our models. We do this by visiting 127.0.0.1:8000/dev/build
.
Let's Review
We have set up our models and a way to administrate them in the CMS. Let's go reviewing a book! Go to https://127.0.0.1:8000/admin
and click on the tab called "Reviews". Start by clicking the tab for Author and start creating an author of a book you like to review (Frank Herbert), then click the tab for books and create the book (Dune). We are now setup for creating a Review. So here's how that looks like:
That's all for this article, but there is room left for improvements. We would like a page for viewing all the reviews on the frontend, and we would like to be able to just review whichever book we want without creating a new entity for each. Those improvements will be left for another day. Please feel free to comment below and let me know what you think.
Top comments (6)
Great tutorial!
However:
1- I set up my containers and confirmed both (ss and db) are working and accessible
2- Created the models and admin model (copied and pasted the exact same code)
3- Ran /dev/build successfully with no errors and confirmed all tables are created
4- Created the first Author instance with no issues
5- Created the first Book instance with no issues (the issue is raised right after book is created)
6- Error is raised preventing me to either: create another Book or go to the Review tab to create a Review.
Error:
ERROR: Uncaught Exception Exception: "SearchField 'Book' on 'App\Model\Review' does not return a valid DBField instance." at /var/www/html/vendor/silverstripe/framework/src/ORM/DataObject.php line 2434 {"exception":"[object] (Exception(code: 0): SearchField 'Book' on 'App\Model\Review' does not return a valid DBField instance.
would really appreciate your feedback on this :) cheers
So I've looked this up now. The problem is the $summary_fields in Review.php. It should be:
So it is the title of the book that will be in the summary fields, not the object Book.
Thanks for the feedback. I'll look into it!
Recently got bumped into install silverstripe blog and found out how ease have they produce into this CMS. Thanks a bunch for this thread. Would definelty use guiding from this one. And Silverstripe is indeed gives an out-of-the-box web-based organization board that empowers clients to modify parts of the website
Great article! 👏
Tack! ^^