In this third article about Bolt CMS we will explore how to work with Bolt CMS as developers. We are going to create a project we callThe Elephpant Experience. We will build a widget for the backend to fetch a random elephant picture and have it being displayed on our homepage. Topics we are going to touch on are:
- Installing Bolt CMS on a development machine
- Building a Controller and add a route for it
- Creating and storing ContentType based from API-calls
- Build a Bolt Widget for controlling when to fetch a new picture
- Modify a theme to display the picture
If you just want the code, you can explore theGithub Repo
Warning! Do not use this code in a production environment. We do not delve into security issues in this one, but as little homework for you - can you spot what would need to be secured in the code?
Answer
The routes are open for anyone! We can solve this by adding a single row to the beginning of the
storeImage
-method in our controller:$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
Installing Bolt CMS on a development machine
Requirements
On theGetting started page of Bolt CMS we find most of any questions we have on how to set up our local development environment. We are going to be using Bolt version 5 and its basic requirements are PHP >7.2, SQLite, and a few PHP-extensions.
PHP-extensions
- pdo
- openssl
- curl
- gd
- intl (optional but recommended)
- json
- mbstring (optional but recommended)
- opcache (optional but recommended)
- posix
- xml
- fileinfo
- exif
- zip
We can runphp -m
to see a list of all the extensions installed and enabled. If anything is missing, it may be a question of enabling them inphp.ini
or otherwise download them fromPECL.
If you find it difficult setting up PHP-extensions on a Windows machine, you can find aninstruction of installing extensions in the official PHP docs. Feel free to ask any questions about this, I know I had many when starting out.
CLI tools
- Composer - A tool for managing dependencies for PHP projects.
- Symfony CLI - A tool for managing Symfony projects, comes bundled with a great virtual server.Nice to have.
Installing Bolt CMS
Having Composer as a CLI tool we can use it to install Bolt CMS. From a terminal we runcomposer create-project bolt/project elephpant-experience
. This will install the latest stable version of Bolt CMS into a new folder calledelephpant-experience
.
Moving into the folder we just created, we can change any configurations needed. For the purpose of this article we will leave the defaults as they are. If you don't want to use SQLite you need to change the .env file to use a different database.
Next up we will initialize our database and populate it with some content. Let's add an admin user and then leverage the fixtures that Bolt provides. We do this withphp bin/console bolt:setup
.
We can now launch the project. We can runsymfony serve -d
directly from our project-root orphp -S localhost:8000
from./public
. When the server is running we can visitlocalhost:8000
in our browser. The first load of the page will take a few seconds to load. This will build up a cache so the next time we visit the page it will be faster.
A benefit of using
symfony serve
is that it provides us with SSL-certificate if that is something you would prefer to have. It will prompt you with a notice if you have not installed a certificate for Symfony and the necessary command to run if you want it.
Good job! We have successfully installed Bolt CMS on our development machine ✔
Building a Controller and add a route for it
As Bolt CMS is built on top of Symfony there are many things we can do the Symfony-way. Let's start by creating the fileElephpantController.php
in thesrc/Controller/
folder. We will use the@Route
annotation for each function we want to return a response. So here's a basic example:
<?phpnamespaceApp\Controller;useSymfony\Component\HttpFoundation\Response;useSymfony\Component\Routing\Annotation\Route;classElephpantController{/** * @Route("/random-elephpant", name="random_elephpant") */publicfunctionindex(){returnnewResponse('Hello Elephpants!');}}
When we visitlocalhost:8000/elephpant
we will see the textHello Elephpants!
. As simple as that, we can add our own logic to our Bolt-project.
The reason Bolt manages to connect the route to our controller is because Bolt has a configuration in
routes.yaml
to look for @Route-annotations in files located in thesrc/Controller/
folder.
Knowing the basics of how to build a controller, we want to expand on it and build a way to fetch and store an elephant picture. It will return a json-response with a status-code. This will be useful when we build a widget for the backend - as we want to know if we were successful or not. We are going to do a simple crawl on Unsplash and fetch a random picture from the result set where we have searched onelephant.
First we are going to get a new dependency to the project. Have Composer get this for us:
composer require symfony/dom-crawler
This dependency will work in tandem with Symfony's HttpClient. This comes with Bolt as one of its dependencies and is used to fetch data from the internet. It can make cURL request which we will use to fetch a response from Unsplash. Before we do anything fancy like storing and building a widget to switch out the image, we are expanding our controller to fetch sush an image randomly and display it on the URI for/random-elephant
. Each time we reload that page, another image will be loaded.
<?phpnamespaceApp\Controller;useSymfony\Bundle\FrameworkBundle\Controller\AbstractController;useSymfony\Component\DomCrawler\Crawler;useSymfony\Component\HttpClient\HttpClient;useSymfony\Component\HttpFoundation\Request;useSymfony\Component\HttpFoundation\Response;useSymfony\Component\Routing\Annotation\Route;useSymfony\Contracts\HttpClient\HttpClientInterface;classElephpantControllerextendsAbstractController{/** * @Route("/random-elephpant", name="random_elephpant") */publicfunctionindex(Request$request,HttpClientInterface$httpClient):Response{$response=$httpClient->request('GET','https://unsplash.com/s/photos/elephants');$crawler=newCrawler($response->getContent());$crawler=$crawler->filterXPath('descendant-or-self::img');$imageArray=[];$crawler->each(function(Crawler$node)use(&$imageArray){$src=$node->attr('src');$alt=$node->attr('alt');if($src&&$alt){$imageArray[]=['src'=>$src,'alt'=>$alt];}});$imageIndex=rand(0,count($imageArray)-1);$imageAtRandom=false;if($imageIndex>0){$imageAtRandom=$imageArray[$imageIndex];}returnnewResponse('<html><body><h1>Elephpant</h1><img src="'.$imageAtRandom['src'].'" alt="'.$imageAtRandom['alt'].'"> </body></html>');}}
This gets us a simple random image:
Putting a pin in the controller for now, we are going to build a contentType of it where we will store the reference of a single random image.
Creating and storing ContentType based on API-calls
We are going to create a ContentType that will be a singleton to store just the URL/source and alt-text of the image we fetched in the previous step. We will call this typeElephpant
. In./config/bolt/contenttypes.yaml
we will create this type:
elephpant:name:Elephpantsingular_name:Elephpantfields:name:type:textlabel:Name of this elephpantsrc:type:textalt:type:textslug:type:sluguses:[name]group:Metadefault_status:publishedicon_one:"fa:heart"icon_many:"fa:heart"singleton:true
After updatingcontenttypes
we want to update Bolt's database to use it. We do this by runningphp bin\console cache:clear
. This will lay the ground for us to be able to store the image-reference in Bolt. Which leads us into the next step where we will do just that.
We return to the controller and itsindex
-function. We will continue to fetch an image when we visit the route. We will return a json-response with the image-url and alt-text when there's a GET-request. Upon a POST-request (which we will use later on) we will store the image-url and alt-text in Bolt. Let's build this out in the controller:
<?phpnamespaceApp\Controller;useBolt\Factory\ContentFactory;useSymfony\Bundle\FrameworkBundle\Controller\AbstractController;useSymfony\Component\DomCrawler\Crawler;useSymfony\Component\HttpClient\HttpClient;useSymfony\Component\HttpFoundation\Request;useSymfony\Component\HttpFoundation\Response;useSymfony\Component\Routing\Annotation\Route;useSymfony\Contracts\HttpClient\HttpClientInterface;classElephpantControllerextendsAbstractController{/** * @Route("/random-elephpant", name="random_elephpant", methods={"GET"}) */publicfunctionindex(Request$request,HttpClientInterface$httpClient):Response{$response=$httpClient->request('GET','https://unsplash.com/s/photos/elephants');$crawler=newCrawler($response->getContent());$crawler=$crawler->filterXPath('descendant-or-self::img');$imageArray=[];$crawler->each(function(Crawler$node)use(&$imageArray){$src=$node->attr('src');$alt=$node->attr('alt');if($src&&$alt){$imageArray[]=['src'=>$src,'alt'=>$alt];}});$imageIndex=rand(0,count($imageArray)-1);$imageAtRandom=false;if($imageIndex>=0){$imageAtRandom=$imageArray[$imageIndex];}if($imageAtRandom){return$this->json(['status'=>Response::HTTP_OK,'image'=>$imageAtRandom,]);}return$this->json(['status'=>Response::HTTP_NOT_FOUND,'message'=>'No image found',]);}/** * @Route("/random-elephpant", name="store_elephpant", methods={"POST"}) */publicfunctionstoreImage(Request$request,ContentFactory$contentFactory):Response{$query=$request->getContent();$query=json_decode($query,true);$src="";$alt="";if(isset($query['src'])){$src=$query['src'];}if(isset($query['alt'])){$alt=$query['alt'];}if($src&&$alt){$elephpantContent=$contentFactory->upsert('elephpant',['name'=>'Random Elephpant',]);$elephpantContent->setFieldValue('src',$src);$elephpantContent->setFieldValue('alt',$alt);$elephpantContent->setFieldValue('slug','random-elephpant');$contentFactory->save($elephpantContent);return$this->json(['status'=>Response::HTTP_OK,'image'=>['src'=>$src,'alt'=>$alt]]);}return$this->json(['status'=>Response::HTTP_INTERNAL_SERVER_ERROR]);}}
In this updated version of the controller we specify that the index-version will be used forGET
-requests and thestoreImage
-version forPOST
-requests. We also use theContentFactory
to create a newelephpant
-content. We then save the content and return a json-response with the status. This will be used in a widget for the admin interface, which we are going to build next.
Build a Bolt Widget for controlling when to fetch a new picture
Next up is for us to build a widget to show on the dashboard. This will enable us to control when to switch out our random image. For this to work, three different parts are required of us. One is theelephpant.html.twig
file, which will be used to render the widget. The other two are theElephpantExtension
andElephpantWidget
files which will be responsible to inject this template to the admin interface. We start with thetwig
-template.
The twig-template will be responsible for displaying our widget, asynchroneously fetch a random image (with the help of our controller) and display it, and also asynchroneously store the image (again with the help of our controller) if we like it.
./templates/elephpant-widget.html.twig
<style>.elephpantimg{width:auto;height:10rem;max-width:20rem;}</style><divclass="widget elephpant"><divclass="card mb-4"><divclass="card-header"><iclass="fas fa-plug"></i>{{extension.name}}</div><divclass="card-body"><p>Update the random image?</p>{%setcontentelephpant='elephpant'%}<div><p>Current Image</p><divclass="image card-img-top"><divid="elephpant-img-container">{%ifelephpantisdefinedandelephpantisnotnull%}<imgsrc="{{elephpant.src}}"alt="{{elephpant.alt}}"/>{%else%}<p>No image</p>{%endif%}</div></div></div><divclass="mt-4"><buttonclass="btn btn-secondary"onclick="fetchElephpantImg()">Fetch new image</button><div><divid="elephpant-img-preview"></div><buttonid="elephpant-img-store"class="btn btn-secondary d-none"onclick="storeElephpantImg()">Store</button></div></div></div></div></div><script>constelephpantImgContainer=document.getElementById('elephpant-img-container');constelephpantImgPreview=document.getElementById('elephpant-img-preview');conststoreButton=document.getElementById('elephpant-img-store');/** * Fetch a new image from our controller's GET-route */functionfetchElephpantImg(){fetch('/random-elephpant').then(response=>response.json()).then(data=>{elephpantImgPreview.innerHTML=`<img src="${data.image.src}" alt="${data.image.alt}" />`;storeButton.classList.remove('d-none');}).catch(error=>{console.error(error);});}/** * Store the current preview-image through our controller's POST-route */functionstoreElephpantImg(){constelephpantImg=elephpantImgPreview.querySelector('img');constrandomImg=elephpantImgPreview.querySelector('img');constrandomImgSrc=randomImg.getAttribute('src');constrandomImgAlt=randomImg.getAttribute('alt');fetch('/random-elephpant',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({src:randomImgSrc,alt:randomImgAlt})}).then(response=>response.json()).then(data=>{storeButton.classList.add('d-none');elephpantImgContainer.innerHTML=`<img src="${data.image.src}" alt="${data.image.alt}" />`;elephpantImgPreview.innerHTML='';}).catch(error=>{console.error(error);});}</script>
Next we need to create theElephpantExtension
-class. This class will be responsible for registering our widget. We create a new folder calledExtension
in oursrc
-folder and create a newElephpantExtension.php
-file there:
<?phpnamespaceApp\Extension;useApp\Extension\ElephpantWidget;useBolt\Extension\BaseExtension;classElephpantExtensionextendsBaseExtension{publicfunctiongetName():string{return'elephpant extension';}publicfunctioninitialize($cli=false):void{$this->addWidget(newElephpantWidget($this->getObjectManager()));}}
If you have intellisense or a reasonable sane IDE, you should get some kind of warning because we do not have the ElephpantWidget-class yet. Let's fix that. In the same folder, create theElephpantWidget.php
-file:
<?phpnamespaceApp\Extension;useBolt\Widget\BaseWidget;useBolt\Widget\Injector\RequestZone;useBolt\Widget\Injector\AdditionalTarget;useBolt\Widget\TwigAwareInterface;classElephpantWidgetextendsBaseWidgetimplementsTwigAwareInterface{protected$name='Elephpant Experience';protected$target=ADDITIONALTARGET::WIDGET_BACK_DASHBOARD_ASIDE_TOP;protected$priority=300;protected$zone=REQUESTZONE::BACKEND;protected$template='@elephpant-experience/elephpant-widget.html.twig';}
This file is pretty minimal and is responsible for directing Bolt on where to use its Widget and how to render it. We set thetemplate
-property to theelephpant-widget.html.twig
-file, which is located in the@elephpant-experience
-namespace. This namespace is directing to the project root'stemplates
-folder. Just be sure to update the namespace if you choose to change the$name
-property of this file.
On the backend we will have the following experience in the dashboard:
We are now on the final stretch. For the last touch we are going to display our image when there is one. We will add it to the homepage for the theme of our choice.
Set up a theme to display the picture
The default theme for Bolt is currentlybase-2021
. Usually we would copy this theme and make it our own, but for this tutorial we will modify it directly. Go to./public/theme/base-2021/index.twig
and add the following after the secondinclude
, on line 8, add:
{# The ELEPHPANT Experience #}{%setcontentelephpant='elephpant'%}{%ifelephpantisdefinedandelephpantisnotnull%}<sectionclass="bg-white border-b py-6"><divclass="container max-w-5xl mx-auto m-8"><divclass="flex flex-col"><h3class="text-3xl text-gray-800 font-bold leading-none mb-3 text-center">The Elephpant Experience</h3>{%ifelephpant%}<divclass="w-full sm:w-1/2 p-6 mx-auto"><imgclass="w-full object-cover object-center"src="{{elephpant.src}}"alt="{{elephpant.alt}}"></div>{%endif%}</div></div></section>{%endif%}
The Bolt-2021 theme uses Tailwind CSS, therefore we see its utility classes being used here. We get the elephpant contentType and display the image if it exists by using
setContent
. Then we use the elephpant variable to accesssrc
andalt
for the image.
We have finally arrived at the goal. We have:
- Installed Bolt CMS
- Built a Controller for our routes
- Created a custom ContentType
- Used a web crawler to fetch a random image (of elephants)
- Added a route for storing the image
- Built a dashboard widget
- Display the random image on our homepage
It's quite a lot, and if you have any questions or viewpoints you are more than welcome to share them. That's the wrap-up, thank you for reading aboutThe Elephpant Experience.
Top comments(2)

- EducationMaster of Space Studies / Master of physics for teaching
- WorkFreelance web dev
- Joined
Hey Anders,
That was absolutely great, thank you.
I really felt in love with Bolt, but it's documentation is very thin when it comes to add functionalities. Your article really helped me.
I would really love to read more on this topic.

- LocationStockholm, Sweden
- Joined
Thanks Thomas! I wouldn't mind exploring how it has evolved over the last couple of years. I've been up in arms with Sylius and SilverStripe lately, so it would be about time to do it.
For further actions, you may consider blocking this person and/orreporting abuse