how to generate Frontend URI in Scheduler Command (TYPO3 9) - typo3

What is the best way to generate frontend URIs in a scheduler command in TYPO3 v9.
I have seen attempts by initializing the TSFE manually, but for me this seems fishy.
Are there any other ways?

The proper way to create links in any context (FE/BE/CLI) is by using the PageRouter. This router is always attached to a site, so you will need to retrieve the correct site first, e.g. by using the SiteFinder. After that you can use PageRouter::generateUri().
Complete example:
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
$site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($pageUid);
$arguments = [
'foo' => 1,
];
// E.g.: "https://example.org/slug-of-page/?foo=1"
$uri = (string)$site->getRouter()->generateUri((string)$pageUid, $arguments);
Notice that this API knows nothing about Extbase and passes through $arguments to the URI so if you need to mimic the behavior of the Extbase UriBuilder you'll need to do that yourself:
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Extbase\Service\ExtensionService;
$objectManager = GeneralUtility::makeInstance(ObjectManager::class);
$extensionService = $objectManager->get(ExtensionService::class);
// E.g. "tx_acme_test"
$argumentsPrefix = $extensionService->getPluginNamespace($extensionName, $pluginName);
$arguments = [
$argumentsPrefix => [
'action' => $actionName, // E.g. "bar"
'controller' => $controllerName, // E.g. "Foo"
'foo' => 42,
],
];
// E.g.: "https://example.org/slug-of-page/?tx_acme_test[action]=bar&tx_acme_test[controller]=Foo&tx_acme_test[foo]=42"
// Or with a route enhancer: "https://example.org/slug-of-page/detail/slug-of-foo"
$uri = (string)$site->getRouter()->generateUri((string)$pageUid, $arguments);

Depending on your needs, the native way described by #mathias-brodala might not be sufficient. In those cases you will need a proper TSFE otherwise you do not have all the TypoScript settings that might influence link generation.
For these cases you basically need to create an instance of the TypoScriptFrontendController yourself, call setup methods and inject real or mocked dependencies like FrontendUserAuthentication depending on your usecase.
A working solution for TYPO3v9, for instance, can be found in the
rx_scheduled_social extension.

Related

How to deploy a test database for DBIx Class

I have Mojolicious app with a test suite using Test::Class::Moose. Using DBIx::Class to interact with my database, is there a way to setup an in-memory database that I can add fixture data to?
I'd like to use an in memory database because it'll mean that the application will have less setup configuration. I do know how to setup an actual SQLite database for testing but managing that for testing along with a mySQL database for production doesn't sound like easy management (eg "oh no, forgot to rebuild the testing database").
Loading data from fixtures seems ideal, that way you have more control over what is actually in the database. Example, you find a bug with a row that contains certain data, add a row like that to your fixture file and test until successful.
Now how to actually set up an in-memory database using DBIx? :)
Right now this is how I'm building the schema for my Test::Class::Moose suite:
has cats => (
is => 'ro',
isa => 'Test::Mojo::Cats',
default => sub { return Test::Mojo::Cats->new(); }
);
has schema => (
is => 'ro',
lazy => 1,
builder => '_build_schema_and_populate',
);
sub _build_schema_and_populate {
my $test = shift;
my $config = $test->cats->app->config();
my $schema = Cat::Database::Schema->connect(
$config->{db_dsn},
$config->{db_user},
$config->{db_pass},
{
HandleError => DBIx::Error->HandleError,
unsafe => 1
}
);
require DBIx::Class::DeploymentHandler;
my $dh = DBIx::Class::DeploymentHandler->new({
schema => $schema,
sql_translator_args => { add_drop_table => 0 },
schema_version => 3,
});
$dh->prepare_install;
$dh->install;
my $json = read_file $config->{fixture_file};
my $fixtures = JSON::decode_json($json);
$schema->resultset($_)->populate($fixtures->{$_}) for keys %{$fixtures};
return $schema;
}
Where my config specifies dbi:SQLite:dbname=:memory: as the database dsn.
When running the test suite, the tables don't seem to be loaded, as I get errors stating the table does not exist, eg Can't locate object method "id" via package "no such table: cats"
Is there some extra setup that I'm not doing when wanting to deploy to an in-memory database?
Thanks
PS:
Doing the following works in a single script, I don't know if I'm doing something that Test::Class::Moose or Mojo doesn't like with the above
#!/usr/bin/perl
use Cats::Database::Schema;
use File::Slurp;
use JSON;
use Data::Dumper;
my $schema = Cats::Database::Schema->connect(
'dbi:SQLite:dbname=:memory:', '', ''
);
my $json = read_file('../t/fixtures.json');
my $fixtures = JSON::decode_json($json);
$schema->deploy();
$schema->resultset($_)->populate($fixtures->{$_}) for keys %{$fixtures};
# returns fixture data fine
# warn Dumper($schema->resultset('User')->search({}));
I believe I figured it out
The way I use the DBIx schema in the app is to instantiate it within a base controller which all sub controllers inherit. No matter how I built and populated the in memory database in the Test::Class::Moose object, it would not be using the instance specified there, instead it would be using the one specified in the base controller.
the solution was to move the schema construction up one level (from controller to the app root) as an attribute, allowing me to override it in Test Mojo to use the in memory db.

mojolicious helper storing an elasticsearch connection

i'm experimenting with elasticsearch within mojolicious.
I'm reasonably new at both.
I wanted to create a helper to store the ES connection and I was hoping to pass the helper configuration relating to ES (for example the node info, trace_on file etc).
If I write the following very simple helper, it works;
has elasticsearch => sub {
return Search::Elasticsearch->new( nodes => '192.168.56.21:9200', trace_to => ['File','/tmp/elasticsearch.log'] );
};
and then in startup
$self->helper(es => sub { $self->app->elasticsearch() });
however if I try to extend that to take config - like the following -
it fails. I get an error "cannot find index on package" when the application calls $self->es->index
has elasticsearch => sub {
my $config = shift;
my $params->{nodes} = '192.168.56.21:' . $config->{port};
$params->{trace_to} = $config->{trace_to} if $config->{trace_to};
my $es = Search::Elasticsearch->new( $params );
return $es;
};
and in startup
$self->helper(es => sub { $self->app->elasticsearch($self->config->{es}) });
I assume I'm simply misunderstanding helpers or config or both - can someone enlighten me?
Just fyi, in a separate controller file I use the helper as follows;
$self->es->index(
index => $self->_create_index_name($index),
type => 'crawl_data',
id => $esid,
body => {
content => encode_json $data,
}
);
that works fine if I create the helper using the simple (1st) form above.
I hope this is sufficient info? please let me know if anything else is required?
First of all, has and helper are not the same. has is a lazily built instance attribute. The only argument to an attribute constructor is the instance. For an app, it would look like:
package MyApp;
has elasticsearch => sub {
my $app = shift;
Search::ElasticSearch->new($app->config->{es});
};
sub startup {
my $app = shift;
...
}
This instance is then persistent for the life of the application after first use. I'm not sure if S::ES has any reconnect-on-drop logic, so you might need to think about it a permanent object is really what you want.
In contrast a helper is just a method, available to the app, all controllers and all templates (in the latter case, as a function). The first argument to a helper is a controller instance, whether the current one or a new one, depending on context. Therefore you need to build your helper like:
has (elasticsearch => sub {
my ($c, $config) = #_;
$config ||= $c->app->config->{es};
Search::ElasticSearch->new($config);
});
This mechanism will build the instance on demand and can accept pass-in arguments, perhaps for optional configuration override as I have shown in that example.
I hope this answers your questions.

Slim Framework - Using a GET route

I'm absolutely new to Slim Framework. I'm working on an Webservice that should provide an interface between an Android App and a Web-Application. I used the Slim Documentation to make my first steps and now I want to create a simple GET route, to receive information from the App. Here is what I have so far:
require 'Slim/Slim.php';
\Slim\Slim::registerAutoloader();
$name_outside = '';
$app = new \Slim\Slim();
$app->get('/session/program_name/:name', function ($name) use($app) {
$name_outside = $name;
echo $name;
});
$app->run();
echo $name_outside;
I need to access the variable :name outside the function, but what I get is nothing. What I am doing wrong here?
Btw: I know that GET-routes usually are used to list existing resources, but for my simple case, I decided to use it that way.
Fix your code to hold name as args parameters ,then you can get in in Your function
require 'Slim/Slim.php';
\Slim\Slim::registerAutoloader();
$name_outside = '';
$app = new \Slim\Slim();
$app->get('/session/program_name/{name}', function ($args) use($app) {
$name_outside = $args['name'];
echo $args['name'];
});
$app->run();
To acess the $name_outside inside the function context you pass it.
$app->get('/session/program_name/:name', function ($name) use($app, &$name_outside) {
$name_outside = $name;
echo $name;
});
But perhaps you're using Slim in a wrong way. Why you have to access the variable outside of your route?
No code is execyted after the run() call. That's the way slim works, try to put a die where you're echo the variable, it isn't reachable.
You shouldn't have the need to access the context of the route outside of it this way. To transform a request or a response you use middlewares with the hooks.

WWW::Mechanize text field issue

I'm trying to submit a form by post method using WWW::Mechanize perl module.
use WWW::Mechanize;
my $mech = WWW::Mechanize->new();
...
$mech->get($url);
...
my $response = $mech->submit_form(
form_name => $name,
fields => {
$field_name => $field_value
},
button => 'Button'
);
$field_name is generally speaking a text field (though the type is not specified explicitly in the form), which has a preset value.
$field_name => $field_value in $mech->submit_form on whatever reason does not replace the value, instead $field_value is added into the form after the original value:
{submitted_field_value} = {original_value},{provided_value}
How to replace {original_value} with {provided_value} in the form to be submitted ?
What happens if you add this single line to your code before calling $mech->submit_form():
$mech->field( $name, [$field_value], 1 );
This makes sure that the first value is added, or overwritten if it already exists.
1 is the number parameter (or position index)
See the documentation of WWW::Mechanize:
$mech->field( $name, \#values, $number )
Given the name of a field, set its value to the value specified. [...]
The optional $number parameter is used to distinguish between two
fields with the same name. The fields are numbered from 1.
It's important to remember WWW::Mechanize is better thought of as a 'headless browser' as opposed to say LWP or curl, which only handle all the fiddly bits of http requests for you. Mech keeps its state as you do things.
You'll need to get the form by using $mech->forms or something similar (its best to decide from the documentation. I mean there so many ways to do it.), and then set the input field you want to change, using the field methods.
I guess the basic way to do this comes out as so:
$mech->form_name($name);
$mech->field($field_name, $field_value);
my $response = $mech->click('Button');
Should work. I believe it will also work if you get the field and directly use that (ie my $field = $mech->form_name($name); then use $field methods instead of $mech.
I managed to make it working at my will. Thanks Timbus and knb for your suggestions. Though my case may not be completely general (I know the preset value) but I'd share what I've found (by trails & errors).
my $mech = WWW::Mechanize->new();
$mech->get($url);
$mech->form_name( $name );
my $fields = $mech->form_name($name);
foreach my $k ( #{$fields->{inputs}}){
if ($k->{value} eq $default_value){
$k->{value}=$field_value;
}
}
my $response = $mech->click('Button_name');

Mojolicious custom sessions

I am trying to use database sessions with Mojolicious instead of the builtin ones that are working with signed cookies.
In the startup subroutine I have something like:
my $dbh = DBI->connect(
$config->{database}->{dsn},
$config->{database}->{user},
$config->{database}->{password},
);
my $session = MojoX::Session->new(
store => [dbi => {dbh => $dbh}], # use MojoX::Session::Store::Dbi
transport => 'cookie', # this is by default
ip_match => 1
);
(ref($self))->attr( 'session' => sub {
return $session;
} );
And I want to use the session object like $self->session or $self->app->session in controllers.
Unfortunately it doesn't work - it uses previous sessions (from different browsers).
This drives me crazy - all I tried today was to make this work - I've read all the documentation available, also the source of MojoX::Session and all its related classes, tried in about 923847293847239847 ways to make it work, but nothing seems to do it.
PS: I created the session table in the db.
Do you know what I should do in order to be able to use DB sessions with Mojolicious?
You can connect MojoX::Session to the application as a plugin in a startup function.
use Mojolicious::Plugin::Session;
[...]
sub startup {
my $self = shift;
[...]
$self->plugin( session => {
stash_key => 'mojox-session',
store => [dbi => {dbh => $dbh}], # use MojoX::Session::Store::Dbi
transport => 'cookie',
ip_match => 1
});
[...]
Afterwards, you'll have access to session through stash key 'mojox-session' in controllers.
For example:
$self->stash('mojox-session')->data('something');
You can use whatever sort of session backend you like. Just create your own controller base class derived from Mojolicious::Controller and override session(), like so:
package NiceController;
use Mojo::Base 'Mojolicious::Controller';
sub session { # custom code here }
1;
then in startup(), set your controller class as the default:
$self->controller_class('NiceController');
Finally, make sure your application controllers inherit from NiceController instead of Mojolicious::Controller
It's probably a good idea to make your overridden session() function work just like the built-in one, to avoid future confusion.
-xyz
The $app->session method is reserved for using the built-in sessions.
You should take a look at the Mojolicious helpers and you probably want to use a different method name to avoid conflict.