-
Notifications
You must be signed in to change notification settings - Fork 0
State management
Index of sub-chapters:
- Underlying logic
-
AbstractStateclass: core of the management -
Subclasses implementation of
AbstractState
As mentioned in the Home page, the Telegram Bot requests are stateless. This means that each request, by default, does not take into account previous ones in its processing. To create complex procedures and recognize, for each request, the state (and therefore the context) in which it must be processed, it is necessary to develop these controls manually.
This framework implements a logic for simple state management.
States indicate the context the bot is in at a given time. Therefore, they indicate the perspective from which the code must see the input requests (message, callback query, etc...) sent by the user, to handle them differently based on the context.
The states, practically, are nothing more than strings that identify the context of the bot request.
Specifically, they are of the form FirstState\InternalState\FinalState (as if they were folder paths in an operating system or a route in a website).
Furthermore, each of these states may need to save data to pass to the next state, so they also have some data to save.
The structure in which to save these states is a database table (*), composed as follows:
| user_idtelegram [PK] | state_name | state_data |
|---|---|---|
| 67756473 | FirstState\InternalState | NULL |
-
user_idtelegramallows you to uniquely connect the state with the Telegram account of the user who is using the bot. Indeed, each state is different for each Telegram user. This 1:1 relationship actually also allows you to insert the other two fields into a possible table in which user data is managed (for example a possibleUserstable that saves all users and them related info) -
state_nameis the name of the user's active state, therefore its context in the bot (in the form described a few lines above) -
state_datacontains any data to be "carried" when switching from one state to another
The definition of the states, which will then turn into the definitions of the classes that will manage them and the namespaces that will identify these classes, is a design operation that should be done in the most rigorous way before starting to work on the bot developing.
(*) You could also use a similar structure, such as a json file or something else. However, a table in a database seems like the most convenient way to handle it to me. Obviously, however, it always depends on the project specifications.
The AbstractState class has only one publicly usable function, namely codeToRun() which is divided internally into 3 parts:
preConditionInput()mainCode()postConditionState()
Into this function the (user) input is validated, i.e. the command that the user sent to the bot and which must be managed by the current state. The command can be of two types:
- static
- dynamic
Static ones, by their very nature, must be written directly into the state-specific class definition, which extends AbstractState:
...
$valid_static_inputs = [
"Command value" => "functionToHandleTheCommand",
"/start" => "startCommand",
"Send Post" => "sendPostCommand",
...
];
...These are checked by the validateStaticInputs() method, which returns true if a match is found between the keys of the $valid_static_inputs array and the message sent by the user to the bot, otherwise false.
The dynamic ones, however, can be various and may not even be present at times.
They are checked after the static ones by the validateDynamicInputs() function which returns false by default, but can be overridden in state-specific subclasses.
If at least one valid match emerges from these two checks, the pre-conditions of the state are exceeded and the $function_to_call attribute must be filled with the value associated with the command which the match occurred with (e.g. if the command is /start then the function to call will be "startCommand"). This assignment is done automatically for static inputs, but must be done manually for dynamic ones!
This function executes the $function_to_call associated with the command found in the pre-conditions phase.
Essentially in the subclasses of AbstractState you will have to worry about defining these functions (associated with the commands). Therefore, is here that the logic of the commands resides, which are the core of the states and of this entire infrastructure.
The work of those who implement the commands lies not only in setting up the entire surrounding environment, but also in creating these functions in the appropriate classes of states.
In this last function/phase the state in the database will have to be changed to prepare the system to the management of the subsequent state.
The state obviously changes based on the command and what is expected to happen in the next steps of the procedure.
For example, it may happen that a procedure ends and therefore you want to return to the initial menu; or it may happen that you need to go to the next step of a procedure and then add a state to the current state path (state_name).
This is the only part of the main state management skeleton in which the AbstractState class deals indirectly (via the $_User class) with the database, changing the state.
Therefore, you need to be careful to call the setNextState($state_name, $state_data=null) function at the end of each function implementation that handles a command.
If you don't call it, the next state will be set to null in the database (which is equivalent to "you are in no specific state", so ideally the initial menu).
If you want to proceed with the states, you will need to write like this:
// (...) implementation of the function
$this->setNextState($this->appendNextState("NextStateName"));It is good to remember that the states names must coincide with existing classes in the database.
The appendNextState(string) method takes the name of the current running state and appends the string to it, preceded by the separator "\".
However, if you want to go back one state, then you will need to do:
// (...) implementation of the function
$this->setNextState($this->getPreviousState());However, this method will take the current state and will remove the last part of the state path, resulting in the name of the statepreceding the current state.
The constructor of the AbstractState class asks for two parameters:
-
_Bot, i.e. the object through which you can interact with the methods of the Telegram bot API -
_User, i.e. the object that allows you to manage all information strictly related to the user Among the information related to the user, in theUserclass, there is also the management of states, through the_StateHandlerclass attribute, which indicates the object capable of interfacing with the database row to delete, modify and create records about state.
There are several considerations to make regarding the implementation of the subclasses of AbstractState.
First of all, they must all be implemented (including AbstractState) in a single folder (I recommend calling it states).
Just outside this folder, a specific autoload function must be implemented to be able to load the state class files when they are encountered during program execution (like states_autoloader.php file inside this project). The autoload is as follows:
<?php
spl_autoload_register(function($class) {
$exploded_classname = explode("\\", $class);
$count_substates = count($exploded_classname);
$relative_classname = $exploded_classname[$count_substates - 1];
$directory_where_search = __DIR__ . DIRECTORY_SEPARATOR . "states" . DIRECTORY_SEPARATOR;
$_RDI = new RecursiveDirectoryIterator($directory_where_search);
$_RII = new RecursiveIteratorIterator($_RDI);
foreach ($_RII as $file) {
if ($file->isDir()){
continuous;
}
$file_path = $file->getPathname();
$file_name = $file->getFilename();
if (is_file($file_path) && $file_name == $relative_classname.".php") {
require_once $file_path;
}
}
});
?>Essentially, the name of the class is taken (parameter $class) (which corresponds to the name of the state path as I will explain in the next paragraph, the relative name of the class is extrapolated ($relative_classname), i.e. its name without the other parts of the namespace.
After that you need to indicate the folder from which to start searching for state classes ($directory_where_search) and the rest of the code will take care of digging recursively in each subfolder until it finds the correspondence with the class file ({$relative_class }.php).
If it finds it, it executes the require_once of the file path to include the state class.
This way if there is a state class repeated several times in different folders, the autoloader will include all classes with the same name, but from different folders.
Idea to implement: the best thing would be to ensure that only the relevant class is included and not all those with the same name
This autoloader is different from the autoloader recommended by PSR-4 because, regarding the individual states classes, the folder structure is not binding.
This means that if I had a class called FirstState\InnerState\FinalState, to be consistent with the PSR-4 autoloader I should create a FirstState folder, with a InternalState folder inside, with the FinalState class inside. Obviously this structure, however tidy, risks becoming a chaotic tangle of folders and paths that are difficult to manage.
This modified autoloader does not prevent you from recreating a similar structure, it simply allows you to handle state classes differently, unlike PSR-4. Indeed, the creation of folders even becomes fundamental in the case in which two states belonging to two different paths have the same name. In this case I could not create two files with the same name to represent two classes, which are in fact different. Then there it is necessary to put them in two different folders, identified by the parent state that best differentiates them.
For example, if I had designed two different states for my bot like CreatePost\SendPost and MergePost\SendPost, I could not insert two SendPost.php files in an hypothetical states/ folder with all the states classes inside. Therefore it's better to create two subfolders, respectively create_post_state/ and merge_post_state/, with the two SendPost.php files specific for the two states inside.
During execution, for the dynamic distinction of the two classes it is still essential to compare the namespace.
The classes must be created in the states folder (states, as suggested). Subfolders can also be created to have the possibility of defining states (therefore classes, therefore files) with the same name but belonging to different paths (as I explained better in the previous paragraph).
As I mentioned previously, the first thing to do in the file of each specific class of a state is to insert the correct namespace, which corresponds to the series of states to which the state you want to implement belongs. For example:
namespace FirstState\InternalState;
class FinalState extends AbstractState {
// ...
}The FirstState, following the same logic, will have no namespace.
Once this is done, the things to set are:
- the
$valid_static_inputsattribute to insert all possible valid static inputs for the state you want to implement; - (optional) override the
validDynamicInputs()method if there are dynamic inputs to check in the state; - all the functions associated with the inputs (static and dynamic) present in the class (and therefore specific to the state);
Final example:
namespace FirstState;
class InternalState extends AbstractState {
protected $valid_static_inputs = [
"/send_name" => "sendName",
"<- back" => "back"
];
protected function sendName() {
// ...
$this->setNextState($this->appendNextState("FinalState"));
}
protected function back() {
// ...
$this->setNextState($this->getPreviousState());
}
}