Adding a form action to a config page - php

I've been trying to make an extension to add some function to the CMS. As it's a setting for the CMS I've added it to the settings tab. While I can take values and save them I needed an action on the page to synchronise a system and I can't get my action to be called, here is my code.
private static $db = array(
'Path' => 'Varchar(50)',
);
private static $allowed_actions = array (
'update',
);
public function updateCMSFields(FieldList $fields)
{
$fields->addFieldsToTab('Root.Importer', array(
ImporterPathField::create('Path', 'Path')->setDescription('Path to area'),
FormAction::create('update', 'Synchronise')
));
}
public function update() {
SS_Log::add_writer(new SS_LogEmailWriter('test#example.com'), SS_Log::ERR);
}
It doesn't get called. If I need to add the function to the left nav rather than part of the settings I'm ok with that too but I also tried that with even less success. Is it possible to get the action called on button press?

You need to place the $allowed_actions and the update method in an extension for CMSSettingsController. Also you should probably put the FormAction into the CMSActions list.
Here's how I would do this:
SiteConfigExtension.php
class SiteConfigExtension extends DataExtension
{
private static $db = array(
'Path' => 'Varchar(50)',
);
public function updateCMSFields(FieldList $fields)
{
$fields->addFieldsToTab('Root.Importer', array(
ImporterPathField::create('Path', 'Path')->setDescription('Path to area')
));
}
public function updateCMSActions(FieldList $actions)
{
$actions->push(
FormAction::create('update', 'Synchronise')
);
}
}
CMSSettingsControllerExtension.php
class CMSSettingsControllerExtension extends DataExtension
{
private static $allowed_actions = array (
'update',
);
public function update() {
SS_Log::add_writer(new SS_LogEmailWriter('test#example.com'), SS_Log::ERR);
}
}

Related

Empty route leeds to 404-Error - SilverStripe 3.5

Like superficial descriped in SilverStripe Docs, I'm trying to set a custom controller for my homepage.
I changed the default homepage link to 'custom-home' and added those two routes.
The second one, with the path in it works and directs me to my controller. The first (empty) one just sends me to an 404-error page.
Couldn't figure out how to fix that. Any suggestions?
routes.yml
Director:
rules:
'': 'MyHome_Controller'
'custom-home': 'MyHome_Controller
_config.php
RootURLController::set_default_homepage_link('custom-home');
MyHome_Controller.php
<?php
class MyHome_Controller extends Page_Controller {
private static $allowed_actions = [];
private static $url_handlers = [];
public function init() {
parent::init();
}
public function Link($action = null) {
return Director::baseURL() . 'custom-home';
}
public function index() {
$data = [
'Title' => 'Hello World',
'ClassName' => __CLASS__,
];
return $this
->customise($data)
->renderWith([__CLASS__, 'Page']);
}
}
I believe the way the empty route (RootURLController) works is that you're telling it the URLSegment of a page in the CMS that should resolve to the root URL. So I think what you need to do is go into the CMS and change the URLSegment of your CustomHomePage to 'custom-home'.

Writing tests for controllers with forms

Ever since I started using Zend Framework 3, I had problems with testing my controllers. I'm trying to test my controllers with PhpUnit 5.7 and my controllers depend on Zend Form, which is hydrated with Doctrine's DoctrineObject.
I'm trying to put this as simple as possible, so here's a minimal example of a setup that's giving me headaches:
Controller:
class IndexController extends AbstractActionController {
private $form;
public function __construct(AlbumForm $form) {
$this->form = $form;
}
public function indexAction() {
return ['form' => $this-form];
}
}
ControllerFactory:
class IndexControllerFactory implements FactoryInterface {
public function __invoke(ContainerInterface $container, ...) {
$formManager = $container->get('FormElementManager');
return new IndexController($formManager->get(AlbumForm::class));
}
}
The corresponding view template in albums/index/index.phtml:
<?php
$this->form->prepare();
$this->form->setAttribute('action', $this->url(null, [], true));
$albumFieldset = $this->form->get('album');
?>
<?= $this->form()->openTag($this-form) ?>
<div class="form-group">
<?= $this->formRow($albumFieldset->get('name')) ?>
</div>
<?= $this->form()->closeTag() ?>
The form:
class AlbumForm extends Form {
public function init() {
$this->add([
'name' => 'albumFieldset',
'type' => AlbumFieldset::class,
'options' => [
'use_as_base_fieldset' => true,
],
]);
}
}
The fieldset:
class AlbumFieldset extends Fieldset {
public function init() {
$this->add([
'name' => 'name',
'type' => Text::class,
'options' => [
'label' => 'Name of album',
],
]);
}
}
The FieldsetFactory:
class AlbumFieldsetFactory implements FactoryInterface {
public function __invoke(ContainerInterface $container, ...) {
$objectManager = $container->get(ObjectManager::class);
$fieldset = new AlbumFieldset();
$fieldset->setHydrator(new DoctrineObject($objectManager));
$fieldset->setObject(new Album());
return $fieldset;
}
}
Now, so far everything is working great.
However, when writing tests for this I run into troubles. Let me first show you what I have so far:
class IndexControllerTest extends AbstractHttpControllerTestCase {
protected function setUp() {
parent::setUp();
$this->configureServiceManager($this->getApplicationServiceLocator());
}
private function configureServiceManager(ServiceManager $services) {
$services->setAllowOverride(true);
$services->setService(ObjectManager::class, $this->mockObjectManager()->reveal());
$services->setService('FormElementManager', $this->mockFormManager()->reveal());
$services->setAllowOverride(false);
}
private $objectManager;
private function mockObjectManager() {
$this->objectManager = $this->prophesize(ObjectManager::class);
return $this->objectManager;
}
private $formManager;
private function mockFormManager() {
$this->formManager = $this->prophesize(FormElementManager::class);
$this->formManager->get(AlbumForm::class)->willReturn($this->mockForm()->reveal());
return $this->formManager;
}
private $form;
private function mockForm() {
$this->form = $this->prophesize(AlbumForm::class);
$this->form->prepare()->willReturn(null);
$this->form->setAttribute('action', Argument::type('string'))->willReturn(null);
$this->form->getAttributes()->willReturn([]);
$this->form->get('album')->willReturn($this->mockAlbumFieldset()->reveal());
return $this->form;
}
private $albumFieldset;
private function mockAlbumFieldset() {
$this->albumFieldset = $this->prophesize(AlbumFieldset::class);
$this->albumFieldset->get('name')->willReturn($this->mockName()->reveal());
return $this->albumFieldset;
}
private $name;
private function mockName() {
$this->name = $this->prophesize(Text::class);
$this->name->getLabel()->willReturn('label');
$this->name->getLabelAttributes()->willReturn(['for' => 'name']);
$this->name->getLabelOption('disable_html_escape')->willReturn(false);
$this->name->getLabelOption('always_wrap')->willReturn(false);
$this->name->getLabelOption('label_position')->willReturn('prepend');
$this->name->getName('album[name]');
$this->name->getAttribute('type')->willReturn('text');
$this->name->hasAttribute('id')->willReturn(true);
$this->name->getAttribute('id')->willReturn('name');
$this->name->getAttributes([])->willReturn([]);
$this->name->getValue()->willReturn(null);
$this->name->getMessages()->willReturn([]);
return $this->name;
}
}
This will eventually run without errors. However, I would like to draw your attention to the last few methods, especially mockName(). Most of those definitions are totally default and almost none of them are specified in AlbumFieldset in the beginning (only name is). It is very annoying to write them down for every form input I may have and writing this down actually introduces more errors than it solves. For example, I'm still not sure what the correct label option for always_wrap would be. I actually don't even care about that option, but I have to write something about it in my test, because otherwise the test fails with 'Prophecy\Exception\Call\UnexpectedCallException' with message 'Method call: - getLabelOption("always_wrap") on Double\Zend\Form\Element\Text\P245 was not expected, expected calls were: ....
Therefore, I'm asking you: is there any better way to go about this? A way that does not involve writing 20+ rows for every field I have in my fieldset. If it involves rewriting my controllers/fieldsets/view templates (etc.), that would totally be fine!
Any help is greatly appreciated! Also, this is my very first time asking something in a forum in over eight years of programming, so please bear with me if anything is unclear.
Yours
Steffen
PS: What I have already tried is to give the IndexController null instead of an actual form and simply abort the view template when it detects that the form is null. However, while that worked without that much setup, I was basically just avoiding the view template's logic. Because of that, I was not able to detect errors in the view template. That's not what I want.
edit IndexControllerTest: Change private as protected elsewhere and extends it for your new fields. Each new controller must overwrite methods calling parent::methodname($args) and add the needed code...

What is the best prestashop-way to include tpl in AdminController?

I need interacts with a .tpl file in my adminController class, but when I try to do that, this error appears
Fatal error: Call to undefined method RiddlePageController::getCacheId() in /home/USER/public_html/prestashop/modules/RiddleModule/controllers/admin/RiddlePage.php on line 48
This is my admin controller code:
class RiddlePageController extends AdminController {
public function __construct()
{
$this->html = '';
$this->display = 'view';
$this->meta_title = $this->l('metatitle');
$this->module = "RiddleModule";
parent::__construct();
}
public function initContent()
{
$this->postProcess();
$this->show_toolbar = true;
$this->display = 'view';
$this->meta_title = $this->l('Modulo');
parent::initContent();
}
public function initToolBarTitle()
{
$this->toolbar_title = $this->l('Titulo');
}
public function initToolBar()
{
return true;
}
public function renderView() {
$this->context->smarty->assign(
array(
'img1' => "http://www.free-3dmodels.com/image/Flowers-3D-Model-3662994d.png",
'img2' => "http://www.all3dmodel.com/Images/39.jpg"
)
);
// in return have error "getCacheId"
return $this->display(__FILE__, 'content.tpl', $this->getCacheId());
// return "<b>This works fine!!</b>";
}
my tpl file have only {$img1} and {$img2} for testing.
Maybe I do all wrong, and this is not the best way to make in my own admin page.
Your error is because the AdminController class doesn't have the getCacheId method.
To answer to your question you have to made some little fix.
First (extends ModuleAdminController not AdminController):
class AdminRiddlePageController extends ModuleAdminController
{
}
Then if you want to view your custom tpl, place a view.tpl file in:
prestashop/modules/RiddleModule/views/templates/admin/riddlepage/helpers/view/
or
prestashop/modules/RiddleModule/views/templates/admin/riddle_page/helpers/view/ (I don't remember well if the underscore is necessary)
And your renderView method should be like this:
public function renderView()
{
/* Your code */
/* Use this snippet to assign vars to smarty */
$this->tpl_view_vars = array(
'myvar' => 1,
'secondvar' => true
)
return parent::renderView();
}
AdminController class has not an implementation of display method you use to render TPL.
You can use something like this after set module var:
$this->module->display(_PS_MODULE_DIR_.$this->module->name.DIRECTORY_SEPARATOR.$this->module->name.'.php', 'content.tpl')
Good luck.
As #TheDrot told us, the answer are in using $this->context->smarty->fetch(location), but not in renderList, but in the return statement of renderView is OK and prestashop get the tpl file and load correctly the smarty variables. Ex:
public function renderView(){
$this->context->smarty->assign(
array(
'img1' => "http://www.free-3dmodels.com/image/Flowers-3D-Model-3662994d.png",
'img2' => "http://www.all3dmodel.com/Images/39.jpg"
)
);
return $this->context->smarty->fetch(_PS_MODULE_DIR_ . "RiddleModule/controllers/front/prueba.tpl");
}
The file location isn't important to load the TPL file in this case

ZendFramework 2 - how do you handle from controller the layout head body footers like in ZF1?

How to do the following __construct section shown in ZF1 on the fly in ZF2 way?
I have tried $this->headTitle('..'); by ommiting ->view call, but it still fail by throwing:
Zend\ServiceManager\ServiceManager::get was unable to fetch or create an instance for headTitle
public function __construct() { //init is gone
$this->_helper->layout()->setLayout('brand');
$this->HeadTitle($this->title)->setIndent(8);
$this->view->headMeta()->appendName('keywords', $this->keyword)->setIndent(8);
$this->view->headMeta()->appendName('description', $this->description)->setIndent(8);
$this->view->headMeta()->appendName('Language', 'en')->setIndent(8);
$this->view->headMeta()->appendName('dc.title', $this->title)->setIndent(8);
$this->view->headMeta()->appendName('dc.keywords', $this->keyword)->setIndent(8);
$this->view->headMeta()->appendName('dc.description', $this->description)->setIndent(8);
$this->view->headLink()->appendStylesheet('/css/main.css')->setIndent(8);
$this->view->headLink()->appendStylesheet('/jquery/css/custom-theme/jquery-ui-
1.8.20.custom.css')->setIndent(8);
$post = $this->getRequest()->getPost();
$get = $this->getRequest()->getQuery();
}
You could access to 'renderer' object in your action controller:
public function indexAction()
{
$renderer = $this->getServiceLocator()->get('Zend\View\Renderer\PhpRenderer');
$renderer->headTitle('My title');
return new ViewModel();
}
I got the same question and I have developed an ZF2 plugin to use headTitle like in layout.phtml file.
https://github.com/remithomas/rt-headtitle
public function indexAction(){
$this->headTitle("My website")->setSeparator(" - ")->append("easy ?!");
return new ViewModel();
}
Write a function to handle it for all actions in a controller
protected function setHeadTitle($title = ''){
if(!empty($title)){
$renderer = $this->getServiceLocator()->get('Zend\View\Renderer\PhpRenderer');
$renderer->headTitle($title);
}
}
Use the function in your action
public function loginAction()
{
$this->setHeadTitle("Login");
//write some other codes
}
Write a plugin for all module
class HeadTitlePlugin extends AbstractPlugin
{
public function setHeadTitle($title = '')
{
if (! empty($title)) {
$renderer = $this->getController()->getServiceLocator()->
get('Zend\View\Renderer\PhpRenderer');
$renderer->headTitle($title);
}
}
}
Attach the plugin in module config
'controller_plugins' => array(
'invokables' => array(
'HeadTitlePlugin' => 'Modulename\Controller\Plugin\HeadTitlePlugin'
)
),
Call the plugin function in controller action
public function indexAction()
{
$this->HeadTitlePlugin()->setHeadTitle("Signup");
// other codes
}
Thats all

Self-referential Relationships in PHP Active Record

I am trying to create a tree of pages using PHP Active Record and I seem to be having trouble getting it setup to work correctly.
Here is the code I am using for the Page class:
class Page extends ActiveRecord\Model {
static $belongs_to = array(array('parent_page', 'class_name' => 'Page'));
static $has_many = array(
array('pages')
);
public static function get_top_level_pages() {
return Page::all(array('conditions' => 'parent_page_id = 0'));
}
}
My database table called Pages has the following columns inside:
id
parent_page_id
type
title
meta_title
content
Does anyone know what I am doing wrong here?
I am not sure if this is the "most correct" way to link a class to itself, but it seems to work for me.
First I changed the parent_page_id in the table to just be page_id and I made the class come together by using the following class:
class Page extends ActiveRecord\Model {
static $has_many = array(
array('pages')
);
static $belongs_to = array(array('page', 'class_name' => 'Page'));
public static function get_top_level_pages() {
return Page::all(array('conditions' => 'page_id = 0'));
}
public function get_parent() {
return $this->page;
}
public function get_children() {
return $this->pages;
}
}
I created the get_parent() and get_children() functions because ->page and ->pages did not make sense to me and the functions help clear that up.
I am open to a better or "more correct" solution.
JP
I was also struggling with this (annoying issue).
You where missing the foreign_key (this will simply point to your column name).
I've modified your code and pasted it here below:
class Page extends ActiveRecord\Model {
//make sure that you define the 'foreign_key'
static $belongs_to = array(
array('parent_page', 'class_name' => 'Page', 'foreign_key' => 'parent_page_id')
);
static $has_many = array(
array('pages')
);
public static function get_top_level_pages() {
return Page::all(array('conditions' => 'parent_page_id = 0'));
}
}
$parent_page = Page::find(20)->parent_page; // this works fine

Categories