A recent client has a website built on Concrete5. I’d first run across Concrete5 (C5) about a dozen years ago when I was evaluating free, open source content management systems for my employer. I installed and tinkered with more than half a dozen. We were determining a best-fit for consolidating an unruly number of websites on various platforms down to as a few as possible.
I can’t remember specifically why I eliminated Concrete5 from the list of many solutions I looked at, but I recall the major concerns to be addressed were:
- Ease-of-use by people in business units
- Support for importing content from other content systems in use
- Rapid development of extensions
WordPress, by the way, easily won out over the competitors as a best fit for us at the time, and it probably still is the best fit.
Now, in 2021, I come across Concrete5 again. It’s apparently gone through a lot since then, but it still wouldn’t win the battle of the CMS’ for best fit for that employer. It might win out for ease-of-use. Or it might not: people there don’t like the WordPress block editor, even once they get used to it, and the C5 editor feels kinda the same to me.
For me, the paramount concern is ease of extending, which comes down to three things for community built and supported software:
- Developer documentation in the form of guides and API references with plenty of examples.
- Good community knowledgebase — forums or ticket systems with solutions, etc. — with good search and filtering capabilities
- Consistent, simple design and architecture
Concrete5 fails in all those areas. In general it fails in all those areas because all the resources mix very, very old retired content with current content. You search, you find a solution, and then discover the information is actually 5, 6, or more years old, or applies only to a long-dead previous system design. Or you find an older solution that still works, but then later discover those features have been deprecated (maybe for years without actually being removed).
The C5 design is very loosely based on a model-view-controller (MVC) architecture. I generally like the MVC pattern, but I think it’s awful for open-source, web development for a number of reasons I’ll leave for another (maybe) post. In short, the MVC pattern isn’t definitively prescriptive — it is just a pattern, after all — and so every implementation I’ve come across is nuanced and different in its interpretation of the pattern. That makes a particular implementation particularly (heheh!) difficult to pick up quickly. That, as well as the other usual problems with MVC open source web software, shines in C5, and shines ever more brightly because the documentation is so terribly bad.
And the documentation’s near-total failure might be the thing that most makes C5 development nightmarish. Some pages that I have found that were really good in covering an obsolete version have no equivalent page for the current version. Some of the pages are just downright incomplete. Others are just … wrong. Here’s an example from the page on how to put a CSRF token in your form and validate it:
The astute, and even the not so astute, reader will notice that the sample code never invokes the output() method. If you enter what they have there, you’ll get nothing on your page, and in fact, will likely get an error because there should be a \ in front of Core::. Even if it does run, generate doesn’t emit anything. To do what they describe, you need this line instead:
\Core::make('token')->output('perform_change_password');
They don’t fix it in later copies of the example, either.
It would be one thing if I’d had to go looking for an example of a problem with the documentation, but I didn’t. That just happened to be open in my browser because adding a CSRF token to my forms was the thing I was working on when I was writing this post. That’s just how frequently flawed the documentation is: you can just open a page at random, close your eyes, and point. OK, that’s exaggerating (but not much).
The API reference (auto-generated doc pages) is so bad that even when a method is documented, what is said about it is virtually useless. Here’s a screenshot of something I looked at today as just a close-your-eyes-and-point example:
I was looking up the action() API. I got to the end of the description and asked out loud, “In the context of what?” I was alone in the room, so no one answered me.
But look at the rest of all that, too! Most methods have no documentation whatsoever, and even when they do their parameters don’t, and even when they do, it’s often just re-iterating the name, telling you nothing about exactly what the name means, the types, whatever. So then you go code spelunking. Let’s just look at that action() method:
Lo and behold! It actually can take more than one argument! Who knew? Yet it doesn’t. Sort of — I mean, it says there is one it refers to in the comment as $task, but nothing called $task is ever used. One would presume it would be passed in the open-ended argument list that the $controllerPath value is prepended to, but is it required if you are going to pass through more opaque values to your handler (assuming it does that “in the context of.” or maybe even outside “the context of.”)? It’s also pretty neat how the description says “… with a task and optional parameters…” and makes no mention of an “action,” and yet $action is the only explicit argument and how that differs from $task or what $task might be isn’t even addressed. I guess to find out, I can just go look at that thing it is calling near the end there, with [$this,’url’]. If you don’;t know, that’s how you would call a method named ‘url’ on an instance of the class we are looking at, so back to the documentation for View to see what that says about the url() method:
Oh. It’s deprecated. Huh. Regardless, you use it in a view to generate URLs with tasks and parameters. And actions, whatever they are “in the context of.” Not really helpful, so we look at the code again, and ….
Huh. It just goes off and does another call to a static member in a different class. Note that I still haven’t really learned much of anything about the action() method, except that it’s a method of lightspeed skipping through code (reference to a movie script written as badly as the C5 docs). Incidentally, this is just after that url() function in the code:
I’m not sure what that “// Legacy Items. Deprecated” refers to … hopefully not the stuff that follows, because there are no more comments until the last function in the file, and all the intervening stuff looks pretty important.
At this point, I really don’t care anymore about the details of the action() method. Trial and error would be quicker. But for grins and giggles, let’s go find that URL::to() method being called by the url() method. I’ve been looking at info for class View in the Concrete\Core\View namespace. There’s no URL class that I see in that namespace, so I’ll pop up a level to Concrete\Core. Ah, there is a Url class in there … oh, but wait, it has no to() method, and when I click to view its source, I get directed to a 404 page, and that’s when I notice the case is wrong in the name anyway. Silly me. So, I’ll search the API docs. That doesn’t turn up anything with only capital letters. Some time later unsuccessfully trying to locate that class in the documentation — I’ll spare you the boring details — I’m beginning to think I imagined seeing it, so back to the code I go. This time, since I have C5 installed locally, I just grep -r ‘class URL’ * on my computer at the root of the C5 install directory. I’ll spare you those gory details, too, of winding up down in concrete/vendor/illuminate/support/Facades/URL.php, where I find this trinket:
<?php
namespace Illuminate\Support\Facades;
/**
* @see \Illuminate\Routing\UrlGenerator
*/
class URL extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'url';
}
}
That is the totality of the code file right there. It’s a hook into the Laravel framework (which I really don’t know much of anything about, to be honest). At this point I don’t care about what I was originally looking for (I’d forgotten anyway), I just want to see what the OMGoodness is going on in here and how C5 is using this stuff. I have no earthly idea where to find this integration information in the online docs, if it is even there (which I doubt), so I keep digging on my computer.
I take a wild guess, and go to concrete/vendor/illuminate/support/facades, and there I find a Facade.php file. I open it. My assumption is that this class is using a PHP magic method to infinitely overload names of things extended from Facade. And there we go:
__callStatic. Tracing the code, I see that getFacadeRoot() is diddling around to figure out the right class. At this point we’re getting deep into the Laravel framework components. So I need to hit the Interwebs. After some time of reading about the Laravel monster, and also about Symfony, and glancing at some other stuff involved, I go looking in the C5 code again. But still, I can’t for the life of me figure out exactly where the ‘url’ name referenced in that Facade derived class, URL, is getting registered with the frameworks, but I do find … some things. I started going in circles at that point trying to figure which thing was being registered with a service name like ‘url’ that has a to() method on it, or which thing with a to() method was being registered as the url service. There’s a promising class called Redirect in concrete/src/Routing/Redirect.php, but I’m not sure that’s getting me the class I’m after. Interestingly it also has a conditional in its to() method that in theory could lead to a call to a static function \URL::to, which — as we know at this point — I can’t find anywhere, except as that facade, maybe, but then …. well ….
… if this to() is what the URL::to() of my quest resolves to, then we could potentially have a bit of a problem here.
At this point, I just decided that short of hooking up a PHP debugger that would let me step through a program, I’d probably never figure out exactly what implementation of what was giving me a URL::to(). And I’m still not an inch closer to learning what those parameters for that action() method (remember that?) might be.
And that’s my point.
There’s nothing simple or elegant about the architecture. It’s counter-intuitive and extremely difficult to decipher. It’s dependent on at least Laravel and Symfony (whether just the Symfony components that Laravel packages up with itself or more, I don’t know), but there are also a lot more doodads down in its “vendor” directory. It looks like C5 is built using umpteen frameworks, libraries, etc., etc., in whole or in part (or maybe doesn’t use most of them at all, but chunks are still in there from some bygone day).
The docs are largely useless. You have to resort to reverse engineering code (involving large, complex 3rd party frameworks) and trial and error. I haven’t run across any architecture models or even lists of dependencies. And the forums are barely useful — not because people in the forums don’t know stuff, but the search of past posts ends up like the docs: one tells you to do things one way, the next tells you that’s deprecated, or a better way is another way, and sometimes they lead you through a chain of posts and documentation pages that takes you in circles.
Over all my impression of Concrete5 is: a tangled, convoluted, bloated, badly documented, nightmarish mess.