Base
We explained what a base is here. Now let's take a look at how a base works.
A base is represented by the following classes:
Note: In fact, you have the option to replace these classes with your own classes in a completely transparent way, using the factories system and your own implementation of the interfaces. Thus, you can rebuild/modify what follows to suit your needs without rewriting the Jet library itself.
An overview of all properties and methods can be found in the description of each class. Here we will describe how the base works technically.
Where and how are the bases stored?
The actual base definitions are stored in the ~/application/bases/ directory (unless you configure determine otherwise). If you look in this directory in the sample application distributed with Jet, you will see the following subdirectories:
- admin
- rest
- web
Those are the bases themselves. Now the first important thing:
Directory name = Base ID
(i.e. admin = base with ID admin, web = base with ID web)
You choose the identifiers. The bases and their identifiers chosen in the sample application are only a demonstration of possible use, but are not binding.
You can have X bases of different purposes, with different identifiers... It's up to you and your projects.
In each of these directories you will find several subdirectories. We'll get to them in a moment.
Now let's look at the base_data.php
~/application/bases/**/base_data.php
This is the actual definition of the base - or if you want to configure it. (You can read why it is a PHP file here).
Let's take a look at one of these files, for example the web base definition in our sample application:
return [
'id' => 'admin',
'name' => 'Administration',
'is_secret' => true,
'is_default' => false,
'is_active' => true,
'SSL_required' => false,
'localized_data' => [
'cs_CZ' => [
'is_active' => true,
'SSL_required' => false,
'title' => 'PHP Jet Framework - admin',
'URLs' => [
'jet-web.lc/admin/',
],
'default_meta_tags' => [
],
],
'en_US' => [
'is_active' => true,
'SSL_required' => false,
'title' => 'PHP Jet Framework - admin',
'URLs' => [
'jet-web.lc/admin/en/',
],
'default_meta_tags' => [
],
],
],
'initializer' => [
'JetApplication\\Application_Admin',
'init',
],
];
I won't break down and describe the individual values of the base definition here. The base is primarily handled through classes and their methods. Thus, it is necessary to know the interfaces of these classes.
But what if you want to create a base?
Yes, you can do it manually. But I haven't made a base myself in years. In practice, the base is created by the installer, for example - this is also the case in the sample application. Another option is to use Jet Studio. There is a whole interface for working with bases in this tool - try it out: ;-)
Last but not least, what is in the data depends on what is in the classes. These files are just storage and their structure is not actually binding. Although it's certainly good to be aware of the definitions of bases.
You can manually interfere with the definition file. Nothing prevents you from doing so. For example, on a production environment (where you certainly don't have Jet Studio) you can activate/deactivate a certain localization (language version) of a site, or even the entire base, etc.
Now let's look at the subdirectories:
~/application/bases/**/pages/
This is a key and therefore mandated directory. It contains page definitions. More precisely, it contains subdirectories whose names correspond to the codes of the individual locales of the bases, and these directories already contain the page definitions. But that's another topic.
~/application/bases/**/lyouts/
This directory contains the layouts - that is, the scripts in charge of the page layout.
Layouts can be (and in practice certainly are - in most cases) shared across multiple sites. Let's say you have one layout for the home page, another for articles, another for product detail, and another layout for the e-commerce shopping process. You have one homepage, but the layout for the product detail will definitely be shared (for example, for multiple product display options and so on).
But surely your administration will have completely different layouts. That's why the directory is here - the layouts are in the context of the base and that's why each base has its own directory for layouts.
On the other hand, such a REST API does not need any layout. Same for service scripts. That's why a directory is not mandatory. For example, the rest base in the sample application doesn't really have any layouts and there is no directory.
~/application/bases/**/views/
As the name suggests, it is designed for view scripts. That is, for something that generates some specific output.
And look out! The directory is not intended for all the views you will have in the project. If your application will be modular, the view of articles will be handled by module Content.Articles (for example - the title is just an illustration, nothing binding) and in its views directory will be the necessary view scripts. Believe me, it's better than having everything in one pile which is hard to navigate.
But why is this view directory bound to the base if the modules have their own view directories? It's for the general view. For example, for those views that use the forms system, or the UI generation system. Just everything that is in general context with the base (for example, forms may have a different general form and queue technilogy in the administration and another one e.g. in the e-shop), but it does not apply to a specific module.
And why isn't there a single directory for the whole application for such generic views? There's nothing preventing it from existing in the first place. This too is configurable, and where the generic views are is actually set within the initializer - so it's completely under your control. For example, you can have a view for generating form fields in one place if you want. It's an edit in a few minutes.
Of course, it's very likely that on a website or e-shop, you'll want to have things (here specifically UI/UX) differently than in the administration. And imagine if someone starts modifying the display of some form field in order to change something in the administration, and breaks the e-shop - unintentionally, but it's not good.
This is why the sample application already has these general views in it several times. Once for the installer, then for Jet Studio, then for administration and also for the web. It may seem like a pain, but it's easy to change. It's mostly to prevent potential and very real problems, but it's not dogma.
Using bases in an application
You will need to use the base frequently in your application for a number of things. For example, when you need:
- List of site (or whatever) localizations
- You will work with URLs
- When you develop e.g. e-shop you can have information about currencies, rounding, pricing etc.
It's just the foundation from which a lot of things are based. Let's say you need that aforementioned list of web localizations. Here's how to do it:
foreach( MVC:getBase('web')->getLocales() as $locale_str => $locale ) {
//... ... ...
}
It is possible and it is a functional way, but I would like to warn against this approach!
What if in the future you need to find out where, how and why a given base is used?
I found the method in the sample application to be useful as well. I don't directly use MVC::getBase methods, but I prepare classes for each base in the application space. So for example JetApplication\Application_Web. These classes are used to encapsulate the application base. You will find in them an initializer (very important!), but also a getBase() method. Let's show one particular base class for illustration:
namespace JetApplication;
use Jet\Logger;
use Jet\MVC;
use Jet\MVC_Router;
use Jet\Auth;
use Jet\SysConf_Jet_ErrorPages;
use Jet\SysConf_Jet_Form;
use Jet\SysConf_Jet_UI;
/**
*
*/
class Application_Web
{
public static function getBaseId(): string
{
return 'web';
}
public static function getBase(): MVC_Base
{
return MVC::getBase( static::getBaseId() );
}
public static function init( MVC_Router $router ): void
{
Logger::setLogger( new Logger_Web() );
Auth::setController( new Auth_Controller_Web() );
SysConf_Jet_UI::setViewsDir( $router->getBase()->getViewsPath() . 'ui/' );
SysConf_Jet_Form::setViewsDir( $router->getBase()->getViewsPath() . 'form/' );
SysConf_Jet_ErrorPages::setErrorPagesDir(
$router->getBase()->getPagesDataPath( $router->getLocale() )
);
}
}
So I have all the essentials in one place.
Base initializer, but also a method to get an instance of the base and I continue to use that. Everything is in the application space in if you find another suitable use, this class (or more precisely classes) can be used for other general basic application functions.
Let's get the list of localizations again, but already correctly and as it is done in real life:
foreach( Application_Web::getBase()->getLocales() as $locale_str => $locale ) {
//... ... ...
}
Or we can show an example how to get the correct URL of the homapage base in its Czech version:
Application_Web::getBase()->getHomepage( new Locale('cs_CZ') )->getURL()
(We will show the URL generation better later.)
Thus, in general, it can be recommended to create your own facades for bases, similar to the JetApplication\Application_Web, JetApplication\Application_Admin classes in the sample application. As the application develops further, this may pay off.
Initialization
This is a topic that I have already mentioned several times in passing and that needs to be thoroughly addressed once again.
From the previous chapter you know what an initializer is for. That is, it is called by the router after it finds out what base the request is for and the purpose of the initializer is to configure the system.
Specifically, it is used to set dependencies of those system components that expect to be set by their very nature. Yes, this is what is called dependency injection. And this is where dependencies are set into containers and facades.(Warning! Container is here a purely abstract concept. It's not supposed to be a configuration file or anything like that. No, it's an abstract concept from the OOP world.)
The initializer was already in the previous example, but let's look at it again:
public static function init( MVC_Router $router ): void
{
Logger::setLogger( new Logger_Web() );
Auth::setController( new Auth_Controller_Web() );
SysConf_Jet_UI::setViewsDir( $router->getBase()->getViewsPath() . 'ui/' );
SysConf_Jet_Form::setViewsDir( $router->getBase()->getViewsPath() . 'form/' );
SysConf_Jet_ErrorPages::setErrorPagesDir(
$router->getBase()->getPagesDataPath( $router->getLocale() )
);
}
As you can see, the initializer receives as a parameter an instance of router that is currently performing the evaluation.
This is handy because the router has already identified the base and locale, and this information can be used.
You can still see the aforementioned dependency settings. This is not directly related to MVC and the base, but it is necessary to clarify it now, at least briefly.
Let's take Logger as an example. This is used to log events and operations. For example, when you want to log a successful operation, you use the call Logger::success( ... ). Logger is used (and should be used) frequently. Therefore, its use and calls must be as simple as possible - even primitive.
Of course, Logger needs a backend. Something that actually writes the information to some log - whatever it is (database, files, ...) and wherever it is.
And I guess you won't want to "mix" together the logs of the administration and payment gateway logs of your e-shop. Suppose you will store the logs in a database for easy processing, but at least each log in a different - separate table (completely different technologies of storing information are possible, but let's not complicate it). So it is clear that we will need a different and differently configured logger for each database.
And here in the initializer and this is how you pass and set up the backend to Logger:
Logger::setLogger( new Logger_Web() );
Nowhere else in the application do you address logger initialization and its dependencies. Then if you want to globally replace the backend with a better one, you simply do it in this one place.
As you can see from the example, this principle is of course not unique to Logger, but is a general principle of the Jet platform. Logger served here as an illustrative example.
And let's go back again. This time to the base_data.php file. We'll be interested in this bit of definition:
'initializer' => [
'JetApplication\\Application_Web',
'init',
],
This is how the initializer is written into the definition. The classic PHP way ['class', 'method']. Please note that the full name of the class is there as a string. There is an option to use a nicer write path: application_Web::class. Yes, that's how it's used everywhere else. But here, for technical reasons, it has to be left written in this old-fashioned way.