Working on Typo3 11.5.13
I'm trying to update some data on my pages table after a be_user changed something.
I read something about setting hooks for that purpose but I can't seem to find a good explanation as to how hooks actually function within Typo3 and how to configure one, especially for my purpose.
As far as I can see, this problem I have should be quickly solved but the complexity of the typo3 doc is hindering my progress again. Maybe you can explain how I can accomplish my goal.
Simply put: A backend user is supposed to choose a date in a datepicker and some dateinterval in the settings of a page. After saving(Or even after picking both values) I would like to update the "Next time happening" field the user can see but not change to be updated to the given date plus the dateinterval chosen.
If you have some sort of idea please share it with me.
Generally hooks are not that good documented. Modern Events are easier to find and better commented. However, if I get your use case right, using DataHandler Hooks are they way to go. That mean, every place which are using the DataHandler to save data are then covered. The backend form engine are using DataHandler.
Basic information about hooks in the core documentation:
https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/Events/Hooks/Index.html
How to identify or find hooks, events, signalslots (depending on TYPO3 version):
https://usetypo3.com/signals-and-hooks-in-typo3.html
https://daniel-siepmann.de/posts/migrated/how-to-find-hooks-in-typo3.html
Introduction or "DataHandler" explained:
https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/Typo3CoreEngine/Database/Index.html
Basicly, DataHandler has two main kind of processings:
Data manipulations -> process_datamap()
Actions (move,delete, copy, translate) -> process_cmdmap()
For DataHandler, you register a class only for datamap and/or processmap, not for a concrete hook itself.
// <your-ext>/Classes/Hooks/MyDataHandlerHooks.php
namespace <Vendor>\<YourExt>\Hooks;
class MyDataHandlerHooks {}
// <your-ext>/ext_localconf.php
// -> for cmdmap hooks
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass']['yourextname']
= \Vendor\YourExt\Hooks\MyDataHandlerHooks::class;
// -> for datamap hooks
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass']['yourextname']
= \Vendor\YourExt\Hooks\MyDataHandlerHooks::class;
You need to register your class only for these kind of hooks you want to consume. And you do not have to implement all hooks.
Hooks can be looked up in \TYPO3\CMS\Core\DataHandling\DataHandler (as hooks are normally searched.
Next step would be to find the proper hook for your use case, and simply add that hook method to your class. Naming the hooks are not chooseable for DataHandler hooks.
TYPO3 Core tests contains a test fixture class for DataHandler hooks - which is not complete, but contains at least the most common ones (along with the needed method signatures) since 8.x:
https://github.com/TYPO3/typo3/blob/main/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/Fixtures/HookFixture.php
So you may have to look into the version for your core version to get a feeling how the signature should look for that core version.
Generally I would guess one of these two:
processDatamap_postProcessFieldArray(): Hook with prepared field array, and you can simple add your new stuff to write or update it and it will be saved. Good if you need to change the record directly.
processDatamap_afterDatabaseOperations(): Hook after record has been changed. This is a good startpoint if you need to do other things after saving a record.
Given your usecase, I would tip on the first one, so here a example implementation (in the class and registering as datamap hook as explained above):
// <your-ext>/Classes/Hooks/MyDataHandlerHooks.php
namespace <Vendor>\<YourExt>\Hooks;
class MyDataHandlerHooks {
/**
* #param string|int $id
*/
public function processDatamap_postProcessFieldArray(
string $status, // Status of the current operation, 'new' or 'update'
string $table, // The table currently processing data for
$id, // The record uid currently processing data for,
// [integer] or [string] (like 'NEW...')
array &$fieldArray, // The field array of a record, cleaned to only
// 'to-be-changed' values. Needs to be &$fieldArray to be considered reference.
DataHandler $dataHandler
): void
{
// $fieldArray may be stripped down to only the real fields which
// needs to be updated, mainly for $status === 'update'. So if you
// need to be sure to have correct data you may have to retrieve
// the record to get the current value, if not provided as with new
// value.
if ($table === 'be_users'
&& $status === 'update'
&& array_key_exists('target_field_name', $fieldArray)
) {
$valueToReactTo = $fieldArray['target_field_name'];
if ($valueToReactTo === 'some-check-value') {
// needs not to be there
$fieldArray['update-field'] = 'my-custom-value';
}
}
}
}
Related
Using Symfony, I am displaying a table with some entries the user is able to select from. There is a little more complexity as this might include calling some further actions e. g. for filtering the table entries, sorting by different criteria, etc.
I have implemented the whole thing in an own bundle, let's say ChoiceTableBundle (with ChoiceTableController). Now I would like to be able to use this bundle from other bundles, sometimes with some more parametrization.
My desired workflow would then look like this:
User is currently working with Bundle OtherBundle and triggers chooseAction.
chooseAction forwards to ChoiceTableController (resp. its default entry action).
Within ChoiceTableBundle, the user is able to navigate, filter, sort, ... using the actions and routing supplied by this bundle.
When the user has made his choice, he triggers another action (like choiceFinishedAction) and the control flow returns to OtherBundle, handing over the results of the users choice.
Based on these results, OtherBundle can then continue working.
Additionally, OtherOtherBundle (and some more...) should also be able to use this workflow, possibly passing some configuration values to ChoiceTableBundle to make it behave a little different.
I have read about the "Controller as Service" pattern of Symfony 2 and IMHO it's the right approach here (if not, please tell me ;)). So I would make a service out of ChoiceTableController and use it from the other bundles. Anyway, with the workflow above in mind, I don't see a "good" way to achieve this:
How can I pass over configuration parameters to ChoiceTableBundle (resp. ChoiceTableController), if neccessary?
How can ChoiceTableBundle know from where it was called?
How can I return the results to this calling bundle?
Basic approaches could be to store the values in the session or to create an intermediate object being passed. Both do not seem particularly elegant to me. Can you please give me a shove in the right direction? Many thanks in advance!
The main question is if you really need to call your filtering / searching logic as a controller action. Do you really need to make a request?
I would say it could be also doable just by passing all the required data to a service you define.
This service you should create from the guts of your ChoiceTableBundleand let both you ChoiceTableBundle and your OtherBundle to use the extracted service.
service / library way
// register it in your service container
class FilteredDataProvider
{
/**
* #return customObjectInterface or scallar or whatever you like
*/
public function doFiltering($searchString, $order)
{
return $this->filterAndReturnData($searchString, $order)
}
}
...
class OtherBundleController extends Controller {
public function showStuffAction() {
$result = $this->container->get('filter_data_provider')
->doFiltering('text', 'ascending')
}
}
controller way
The whole thing can be accomplished with the same approach as lipp/imagine bundle uses.
Have a controller as service and call/send all the required information to that controller when you need some results, you can also send whole request.
class MyController extends Controller
{
public function indexAction()
{
// RedirectResponse object
$responeFromYourSearchFilterAction = $this->container
->get('my_search_filter_controller')
->filterSearchAction(
$this->request, // http request
'parameter1' // like search string
'parameterX' // like sorting direction
);
// do something with the response
// ..
}
}
A separate service class would be much more flexible. Also if you need other parameters or Request object you can always provide it.
Info how to declare controller as service is here:
http://symfony.com/doc/current/cookbook/controller/service.html
How liip uses it:
https://github.com/liip/LiipImagineBundle#using-the-controller-as-a-service
I'm using Propel 1 in a fairly sizeable project, and the live version presently uses the Archivable behaviour. Thus, when a row is deleted, the behaviour transparently intercepts the call and moves the row into an archive table. This works fine.
I'm looking to change how this table works so that all saves are versioned. On a feature branch I have therefore removed the Archivable and added the Versionable behaviour. This drops the (table)_archive auto-generated table and adds a (table)_version table instead.
However, interestingly, the version table has a PK of (id, version) with a foreign key to the live table from id to id. This means that versions cannot exist without a live row, which is not what I want: I want to be able to delete a row and preserve the versions.
I thought this behaviour would act like Archivable i.e. the delete() method would be intercepted and modified from its usual approach. Unfortunately, as confirmed by the documentation, this method deletes the live row and any prior versions:
void delete(): Deletes the object version history
I tried mixing both Archivable and Versionable, but this seems to generate code that crashes in the Query API: it tries to call an archive() method that does not exist. I expect this behaviour mix was never intended to work (ideally it should be caught at schema build-time, and perhaps that will be fixed in Propel 2).
One solution is to try the SoftDelete behaviour instead of Archivable - this just marks records as deleted rather than moving them to another table. However this can be problematic because joining to a table with this behaviour can give the wrong counts for non-deleted rows (and the Propel team decided to deprecate it for this reason). It also feels like a rabbit-hole I don't want to go down, since the amount of refactoring may spiral out of control.
Thus, I am left with seeking a better approach to implement a versioning system that does not delete old versions when the live copy is deleted. I can do this manually by intercepting save and delete methods in the model class, but it seems a waste when Versionable nearly does what I want. Are there relevant parameters I can tweak, or is there value in writing a custom behaviour? A quick look at the template generation code for core behaviours makes me want to run away from the latter!
Here is the solution I came up with. My memory is rather hazy but it looks like I've taken the existing VersionableBehaviour and derived from it a new behaviour, which I have called HistoryVersionableBehaviour. It thus uses all the features of the core behaviour and then just overrides the generated delete with its own code.
Here is the behaviour itself:
<?php
// This is how the versionable behaviour works
require_once dirname(__FILE__) . '/HistoryVersionableBehaviorObjectBuilderModifier.php';
class HistoryVersionableBehavior extends VersionableBehavior
{
/**
* Reset the FKs from CASCADE ON DELETE to no action
*
* (I expect all future migration diffs will incorrectly try to re-add the constraint
* I manually removed from the migration that introduced versioning, may try to fix
* that another time. 'Tis fine for now).
*/
public function addVersionTable()
{
parent::addVersionTable();
$this->swapAllForeignKeysToNoDeleteAction();
$this->addVersionArchivedColumn();
}
protected function swapAllForeignKeysToNoDeleteAction()
{
$versionTable = $this->lookupVersionTable();
$fks = $versionTable->getForeignKeys();
foreach ($fks as $fk)
{
$fk->setOnDelete(null);
}
}
protected function addVersionArchivedColumn()
{
$versionTable = $this->lookupVersionTable();
$versionTable->addColumn(array(
'name' => 'archived_at',
'type' => 'timestamp',
));
}
protected function lookupVersionTable()
{
$table = $this->getTable();
$versionTableName = $this->getParameter('version_table') ?
$this->getParameter('version_table') :
($table->getName() . '_version');
$database = $table->getDatabase();
return $database->getTable($versionTableName);
}
/**
* Point to the custom object builder class
*
* #return HistoryVersionableBehaviorObjectBuilderModifier
*/
public function getObjectBuilderModifier()
{
if (is_null($this->objectBuilderModifier)) {
$this->objectBuilderModifier = new HistoryVersionableBehaviorObjectBuilderModifier($this);
}
return $this->objectBuilderModifier;
}
}
This needs something called a modifier, which is run at generation time to produce the base instance classes:
<?php
class HistoryVersionableBehaviorObjectBuilderModifier extends \VersionableBehaviorObjectBuilderModifier
{
/**
* Don't do any version deletion after the main deletion
*
* #param \PHP5ObjectBuilder $builder
*/
public function postDelete(\PHP5ObjectBuilder $builder)
{
$this->builder = $builder;
$script = "// Look up the latest version
\$latestVersion = {$this->getVersionQueryClassName()}::create()->
filterBy{$this->table->getPhpName()}(\$this)->
orderByVersion(\Criteria::DESC)->
findOne(\$con);
\$latestVersion->
setArchivedAt(time())->
save(\$con);
";
return $script;
}
}
The parent class has 798 lines, so my approach does seem to have saved a great deal of code, over building it all from scratch!
You'll need to specify the behaviour in your XML file for each table you want to activate it for:
<table name="job">
<!--- your columns... -->
<behavior name="timestampable" />
<behavior name="history_versionable" />
</table>
I am not sure whether my behaviour requires the presence of the timestampable behaviour - my guess is no, since it looks like the parent behaviour just adds columns to the versioned table and not the table itself. If you are able to try this without the timestampable behaviour do let me know how you get on, so I can update this post.
Finally you'll need to specify the location of your class so the Propel 1 custom autoloader knows where to find it. I use this in my build.properties:
# Declare a custom behaviour
propel.behavior.history_versionable.class = ${propel.php.dir}.WebScraper.Behaviours.HistoryVersionable.HistoryVersionableBehavior
i was trying to setup a general service which handles common function i use very often everywhere in my project. For example if a user wants to purchase something for virtual currency there would be a function which checks, if the user has enough virtual currency in his account.
If the user doesnt have enough virtual currency I want this function to make a JSOn Response, but of cource, only controllers are allowed to response. But this means i have to check in every action I use this function, whether the purchase is valid or not.
Here is the function call in my Controller:
$purchaes= $this->get('global_functions')->payVirtualCurrency($user_id, $currency_amount);
if($change instanceof JsonResponse){
return $change;
}
And the function:
public function payVirtualCurrency($user_id, $currency_amount){
$user = $this->dm->getRepository('LoginBundle:User')->findOneById($user_id);
if($user->getVirtualCurrency() < $currency_amount){
return new JsonResponse(array('error' => $this->trans->trans('Insufficient amount of virtual Currency')));
}
return true;
}
Is there a better way to do this? I really want to avoid doing the same thing in the controller over and over again.
Thanks in advance!
Two options come to my mind, both are quite elegant solutions but both require little work:
1. Create custom exception listener
Create custom exception, let's call it InsufficientMoneyException. Then, your sevice can be as it is, but instead of returning response it throws your custom exception (in case user does not have enough money). Then, you create custom exception listener which listenes to InsufficientMoneyException custom exception and returns your desired JsonResponse.
2. Create custom annotation
You can create custom annotation and flag a controller action with this annotation. It would look something like this
/**
* #MinimumMoneyRequired("50")
*/
public function buyAction()
{
(...)
}
This option is really nice and decoupled but it require quite a lot of configuration. This is nice blog post with detailed description how to create custom annotations
I've created a plugin system for a software in php. In order for a plugin to alter the behaviour of the programm I wrote this (simplified) code:
class PluginController {
/* ... */
public function addHook($name, $function, $priority = 10) {
/* store the function callback $function associated with $name */
}
public function executeHook($name, $args = array()) {
/* execute all function callbacks associated with $name
* in order of their priority and return their results */
}
}
So plugins can add callbacks using addHook and somewhere in the application these callbacks get executed by calling executeHook.
This works quite well, but after reading some time about the topic, I'm still unsure if this technique is a event- or a hook- system.
Some sources say the difference has to do with loose and tight coupling.
Others say that hooks has return values, and events not. And others again say that events are for handlig asynchronous activity, and hooks just to inject code at some point.
So again, is the above code about events or hooks, and can someone explain the difference?
Your code is more like an Event.
Hooks allow a plugin to interact with the code that called it. They are called with the assumption that data will be returned, and the originating code will usually loop through the returned data immediately after calling the hook.
Events, on the other hand, are only called to announce when a particular action has taken place. They give plugins an opportunity to run their own event-handling logic at that point, without directly affecting the originating code in any way.
Source
Let me start off by saying I'm an intermediate level PHP coder who's learning OOP. I've got a site running, but I would like to break my code up to implement a more flexible design pattern...and because OOP is just plain awesome.
In my original code, I used switch statements to call functions that correspond with the user request.
$request = (string) $_GET['fruit'];
switch ($request) {
case 'apply':
Get_Apple();
default:
Error_Not_A_Fruit();
exit;
}
This makes it highly inflexible and requires me to change the code at multiple locations for adding new options the user may request.
I'm thinking about changing it to a Polymorphic class call. I'm using composer, so I've got my Objects setup to autoload with PSR-4 standards. So the answer seems simple, if the user request is "apple," I could create
$request = (string) $_GET['fruit'];
$product = new Product\$request;
But, if a user manually enters something that doesn't exists...say "orange," what method would I use to white list the user's input? Like I said, this is my first venture into OOP and would love to pick up design standards that you guys use. I'm thinking encapsulating the block inside a try{} & catch(){} block, but is that the way it should be done?
Any advise would be greatly appreciated :)
Cheers,
Niro
Update: I'd like to make it clearer because it may not have been before. I'm looking for the approach to doing this in such a way that one could add new Objects of Subclass Product (implementing a Product Interface). That way I can add different Product types without changing code everywhere.
Well, you can always use class_exist('Product\\Apple').
class_exists takes 2 arguments:
Class Name (full, with namespace)
Boolean value, whether to try and autoload the class or not. Default is true.
The function returns a boolean.
So you write
$fullClassName = "Product\\$request";
if(class_exists($fullClassName )) {
$product = new $fullClassName();
}
else {
//error here
}