I'm developing Prestashop module, it will export customer data and orders, it will contain hooks for customer synchronization, cart and order events - generally module which will be an integration with CRM-like service.
My module contains it's own views, made in vue.js - single page, async. There are register, login, settings, etc. pages. Communication with backend is made by GET/POST requests on {baseUrl}/mymodule/actionname routes and simple json responses which vue views depend on. Simply I need to create REST endpoints for my module, something like examples below.
Wordpress custom RestApi:
class RestApi
{
public function __construct()
{
add_action('rest_api_init', array(get_class($this),
'register_endpoints'));
}
public static function register_endpoints()
{
register_rest_route('mymodule', '/login', array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array('RestApi', 'login' ),
));
}
}
SugarCRM custom RestApi:
class ModuleRestApi extends SugarApi
{
public function registerApiRest()
{
return [
'moduleLogin' => [
'reqType' => 'POST',
'noLoginRequired' => true,
'path' => [
'mymodule', 'login'
],
'method' => 'login'
],
];
}
}
I cannot find similar solution in PrestaShop, there is no word about custom endpoints in presta docs, I tried to use FrontModuleControllers with friendly url's but it doesn't seem to work for me, it throws a lot of stuff in response which is useless for me and when I try to override init() method it requires a lot of stuff too to actually initiate the controller. I need simple REST solution where I can put logic for recieving data from my views, pass it to my CRM service and return json responses to my views. I don't need any more templates or views rendering, just routing for cummunication.
PrestaShop doesn't support this out of the box. You can however do it with a module and front controllers.
This is a basic example of doing it.
1. Module to register friendly URLs
class RestApiModule extends Module
{
public function __construct()
{
$this->name = 'restapimodule';
$this->tab = 'front_office_features';
$this->version = '1.0';
parent::__construct();
}
public function install()
{
return parent::install() && $this->registerHook('moduleRoutes');
}
public function hookModuleRoutes()
{
return [
'module-restapimodule-login' => [
'rule' => 'restapimodule/login',
'keywords' => [],
'controller' => 'login',
'params' => [
'fc' => 'module',
'module' => 'restapimodule'
]
]
];
}
}
2. Create an abstract REST controller
Create an abstract controller so that actual endpoints can extend from it. Create it in your module controllers folder lets name it AbstractRestController.php
abstract class AbstractRestController extends ModuleFrontController
{
public function init()
{
parent::init();
switch ($_SERVER['REQUEST_METHOD']) {
case 'GET':
$this->processGetRequest();
break;
case 'POST':
$this->processPostRequest();
break;
case 'PATCH': // you can also separate these into their own methods
case 'PUT':
$this->processPutRequest();
break;
case 'DELETE':
$this->processDeleteRequest();
break;
default:
// throw some error or whatever
}
}
abstract protected function processGetRequest();
abstract protected function processPostRequest();
abstract protected function processPutRequest();
abstract protected function processDeleteRequest();
}
3. Create an actual front controller
Create the front controller in your module controllers/front folder and name it login.php.
require_once __DIR__ . '/../AbstractRestController.php';
class RestApiModuleLoginModuleFrontController extends AbstractRestController
{
protected function processGetRequest()
{
// do something then output the result
$this->ajaxDie(json_encode([
'success' => true,
'operation' => 'get'
]));
}
protected function processPostRequest()
{
// do something then output the result
$this->ajaxDie(json_encode([
'success' => true,
'operation' => 'post'
]));
}
protected function processPutRequest()
{
// do something then output the result
$this->ajaxDie(json_encode([
'success' => true,
'operation' => 'put'
]));
}
protected function processDeleteRequest()
{
// do something then output the result
$this->ajaxDie(json_encode([
'success' => true,
'operation' => 'delete'
]));
}
}
Install the module and now you can hit http://example.com/restapimodule/login and depending on the request type it's going to do whatever you want and you get back JSON response.
To add more endpoints add another module-restapimodule-endpointname entry into hookModuleRoutes array and a front controller that extends from AbstractRestController.
If you also want proper response codes etc. you're going to have to set headers with native php functions as PrestaShop afaik doesn't have any utilities to do it for you or use some kind of library.
Same goes for any other headers you might want to set such as content-type (by default it is text/html).
It is possible to use the Prestashop Webservice, that allows to add resources from modules. This solution could save some time in terms of standards and security.
The documentation regarding module resources in Prestashop Webservice is in this link:
https://webkul.com/blog/creating-prestashop-module-webservice-api/
Related
i Have a question for you guys :) I'm trying to add an API on my yii2 advanced template as i want my wordpress website send datas to my yii2 app.
My system : 1) Yii2 advanced template 2) wordpress website 3) my wordpress plugin with vuejs and axios to create a new entry in my yii2 app via API
So what i allready did :
common/config/main.php (as i use AccessController, i added orders/* to allow it)
'as access' => [
'class' => 'mdm\admin\components\AccessControl',
'allowActions' => [
'orders/*',
],
frontend/config/main.php
'components' => [
'request' => [
'parsers' => [
'application/json' => 'yii\web\JsonParser',
],
and (in urlManager array)
['class'=>'yii\rest\UrlRule','controller'=>'Orders'],
[‘class'=>'yii\rest\UrlRule','controller'=>'Contacts']
Then my controller :
<?php
namespace frontend\controllers;
use yii\rest\ActiveController; use yii\filters\auth\HttpBasicAuth;
class OrdersController extends ActiveController {
public $modelClass = 'common\models\Vctorders';
public function behaviors()
{
$behaviors = parent::behaviors();
// remove authentication filter
$auth = $behaviors['authenticator'];
unset($behaviors['authenticator']);
// add CORS filter
$behaviors['corsFilter'] = [
'class' => \yii\filters\Cors::className(),
];
// re-add authentication filter
$behaviors['authenticator'] = $auth;
// avoid authentication on CORS-pre-flight requests (HTTP OPTIONS method)
$behaviors['authenticator']['except'] = ['options'];
return $behaviors;
}
public function actions()
{
$actions = parent::actions();
unset($actions['create']);
unset($actions['update']);
unset($actions['delete']);
unset($actions['view']);
//unset($actions['index']);
return $actions;
}
protected function verbs(){
return [
'create' => ['POST'],
'new' => ['POST'],
'update' => ['PUT', 'PATCH','POST'],
'delete' => ['DELETE'],
'view' => ['GET'],
//'index'=>['GET'],
];
}
public function actionCreate()
{
$model = new Vctorders();
$model->date_creation = date('Y-m-d H:i:s',strtotime('now'));
$model->etat = 0;
if($model->save()){
return 'OK';
} else{
return 'error';
}
}
}
So, i a use Postman with : http://localhost:8888/SD/sdms/orders/ i get a record, no problem
But when i do a POST with :
http://localhost:8888/SD/sdms/orders/create?livre=L'Arbre Musicien&langue=Allemand&nom=Perroud&prenom=LIttledave&nombre=2&npa=1221&pays=suisse&accept_pc=1&mail=post#post.ch&etat=1&message=lbablalbal&tel=01201201212
the answer is
{"name":"Exception","message":"Class 'frontend\\controllers\\Vctorders' not found","code":0,"type":"Error","file":"/Applications/MAMP/htdocs/SD/sdms/frontend/controllers/OrdersController.php","line":58,"stack-trace":["#0 [internal function]: frontend\\controllers\\OrdersController->actionCreate()","#1 /Applications/MAMP/htdocs/SD/sdms/vendor/yiisoft/yii2/base/InlineAction.php(57): call_user_func_array(Array, Array)","#2 /Applications/MAMP/htdocs/SD/sdms/vendor/yiisoft/yii2/base/Controller.php(157): yii\\base\\InlineAction->runWithParams(Array)","#3 /Applications/MAMP/htdocs/SD/sdms/vendor/yiisoft/yii2/base/Module.php(528): yii\\base\\Controller->runAction('create', Array)","#4 /Applications/MAMP/htdocs/SD/sdms/vendor/yiisoft/yii2/web/Application.php(103): yii\\base\\Module->runAction('orders/create', Array)","#5 /Applications/MAMP/htdocs/SD/sdms/vendor/yiisoft/yii2/base/Application.php(386): yii\\web\\Application->handleRequest(Object(yii\\web\\Request))","#6 /Applications/MAMP/htdocs/SD/sdms/frontend/web/index.php(17): yii\\base\\Application->run()","#7 {main}"]}
The problem is you are sending your request in POSTMAN as a GET REQUEST, and your ACTION in the CONTROLLER expects a POST REQUEST.
In your POSTMAN client at the left side, there is a selector where you can choose what kind of request you are doing.
The second error you get is because once you remove the requisite of POST, the GET request enters the action and goes here:
} elseif (!\Yii::$app->request->isPost) {
$model->load(Yii::$app->request->get());
}
And tries to load the model with the data in the GET parameters, but fails as there is no way the default $model->load() knows how to map the data in your GET petition.
In any case (GET or POST) the $model->load() will not work, as if you check the load() function you will find that it searches for the Object modal name inside the array to load the parameters, so you must do like:
http://localhost:8888/SD/sdms/orders/create?Orders%5Blivre%5=L'Arbre
But for each parameter, the strange characters you see in the result of stringify ['Orders' => ['livre' => 'Larbre']].
I created new resources with this code:
class WebserviceRequest extends WebserviceRequestCore {
public static function getResources(){
$resources = parent::getResources();
// if you do not have class for your table
$resources['test'] = array('description' => 'Manage My API', 'specific_management' => true);
$resources['categoryecommerce'] = array('description' => 'o jacie marcin', 'class' => 'CategoryEcommerce');
$mp_resource = Hook::exec('addMobikulResources', array('resources' => $resources), null, true, false);
if (is_array($mp_resource) && count($mp_resource)) {
foreach ($mp_resource as $new_resources) {
if (is_array($new_resources) && count($new_resources)) {
$resources = array_merge($resources, $new_resources);
}
}
}
ksort($resources);
return $resources;
}
}
And new class:
class CategoryEcommerceCore extends ObjectModelCore {
public $category_id;
public $category_core_id;
public static $definition = array(
'table' => "category_ecommerce",
'primary' => 'category_id',
'fields' => array(
'category_core_id' => array('type' => self::TYPE_INT),
)
);
protected $webserviceParameters = array();
}
Webservice is override properly. My class WebserviceRequest is copying to
/override/classes/webservice/WebserviceRequest
but class isn't copying to /override/classes/ when i installing my module.
How to add new resourcess with own logic ? I want to add categories within relation to my table.
Regards
Martin
As soon as there is literally nothing regarding the API except Webkul tutorial... I tried to implement the "Webkul's" tutorial, but also failed. However seems that it's better to use hooks instead of overrides. I used my "reverse engineering skills" to determine the way to create that API, so-o-o-o, BEHOLD! :D
Let's assume you have a custom PrestaShop 1.7 module. Your file is mymodule.php and here are several steps.
This is an install method wich allows you to register the hook within database (you can uninstall and reinstall the module for this method to be executed):
public function install() {
parent::install();
$this->registerHook('addWebserviceResources');
return true;
}
Add the hook listener:
public function hookAddWebserviceResources($resources) {
$added_resources['test'] = [
'description' => 'Test',
'specific_management' => true,
];
return $added_resources;
}
That specific_management option shows you are going to use WebsiteSpecificManagement file instead of database model file.
Create WebsiteSpecificManagement file, called WebsiteSpecificManagementTest (Test - is CamelCased name of your endpoint). You can take the skeleton for this file from /classes/webservice/WebserviceSpecificManagementSearch.php. Remove everything except:
setObjectOutput
setWsObject
getWsObject
getObjectOutput
setUrlSegment
getUrlSegment
getContent (should return $this->output; and nothing more)
manage - you should rewrite it to return/process the data you want.
Add
include_once(_PS_MODULE_DIR_.'YOURMODULENAME/classes/WebserviceSpecificManagementTest.php');
to your module file (haven't figured out how to include automatically).
Go to /Backoffice/index.php?controller=AdminWebservice and setup the new "Auth" key for your application, selecting the test endpoint from the permissions list. Remember the key.
Visit /api/test?ws_key=YOUR_KEY_GENERATED_ON_STEP_4 and see the XML response.
Add &output_format=JSON to your URL to see the response in JSON.
You have to use something like $this->output = json_encode(['blah' => 'world']) within manage method at WebsiteSpecificManagementTest.
Here is how my RESTful controller looks like.
<?php
namespace backend\controllers;
use yii\rest\Controller;
use yii;
use yii\web\Response;
use yii\helpers\ArrayHelper;
class UserController extends \yii\rest\ActiveController
{
public function behaviors()
{
return ArrayHelper::merge(parent::behaviors(), [
[
'class' => 'yii\filters\ContentNegotiator',
'only' => ['view', 'index'], // in a controller
// if in a module, use the following IDs for user actions
// 'only' => ['user/view', 'user/index']
'formats' => [
'application/json' => Response::FORMAT_JSON,
],
'languages' => [
'en',
'de',
],
],
[
'class' => \yii\filters\Cors::className(),
'cors' => [
'Origin' => ['*'],
'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
'Access-Control-Request-Headers' => ['*'],
'Access-Control-Allow-Credentials' => true,
'Access-Control-Max-Age' => 86400,
],
],
]);
}
public $modelClass = 'backend\models\User';
public function actions()
{
}
public function sendMail(){
//Need to call this function on every create
//This should also have the information about the newly created user
}
}
It works very well with default behavior but it is not very practical that you will just create the user and exit. You need to send email with verification link SMS etc, may be update some other models based on this action.
I do not want to completely override the create method as it works well to save data and return back JSON.
I just want to extend its functionality by adding a callback kind of a function which can accept the newly created user and send an email to the person.
Take a look here: https://github.com/githubjeka/yii2-rest/blob/bf034d26f90faa3023e5831d1eb165854c5c7aaf/rest/versions/v1/controllers/PostController.php
As you can see this is using the prepareDataProvider to change the normal way the index action is using. This is very handy. You can find the definition of prepareDataProvider here: http://www.yiiframework.com/doc-2.0/yii-rest-indexaction.html#prepareDataProvider()-detail
Now as you can see there are 2 additional methods afterRun() and beforeRun() that are also available for the create action. http://www.yiiframework.com/doc-2.0/yii-rest-createaction.html
You may be able to use these 2 functions and declare them similar to prepareDataProvider to do more things like sending an email. I have not tried them myself but I believe that should be the way to go.
The easiest way would be getting benefit from afterSave() method in your model. This method will be called after each save process.
public function afterSave($insert, $changedAttributes) {
//calling a send mail function
return parent::afterSave($insert, $changedAttributes);
}
Another advantage of this method is the data you have stored in your object model. For example accessing email field:
public function afterSave($insert, $changedAttributes) {
//calling a send mail function
\app\helpers\EmailHelper::send($this->email);
return parent::afterSave($insert, $changedAttributes);
}
the value of $this->email is containing the saving value into database.
Note
You can benefit from $this->isNewRecord to detect whether the model is saving new record into database or updating an existing record. Take a look:
public function afterSave($insert, $changedAttributes) {
if($this->isNewRecord){
//calling a send mail function
\app\helpers\EmailHelper::send(**$this->email**);
}
return parent::afterSave($insert, $changedAttributes);
}
Now, it only sends mail if new record is being saved into database.
Please note that you can also benefit from Yii2's EVENTS.
As official Yii2's documentation:
This method is called at the end of inserting or updating a record.
The default implementation will trigger an EVENT_AFTER_INSERT event when $insert is true, or an EVENT_AFTER_UPDATE event if $insert is false. The event class used is yii\db\AfterSaveEvent. When overriding this method, make sure you call the parent implementation so that the event is triggered.
Yii is giving me 404 Error if I declare an action like this:
SiteController.php
public function actionRegisterUser()
This is how I call it in the main.php
['label' => 'Register User', 'url' => ['/site/RegisterUser']],
I tried several different combinations. The only combination that will work is this naming convention in both places:
public function actionRegisteruser
'url' => ['/site/registeruser']
I used to work on another Yii project (Yii 1.0) and I could name my actions in camel case and call them without any problem. Do I need to turn on some sort of setting to do this?
I also tried playing with the rules of the Controller but that didn't solve anything.
In some cases you need camelcase link. For example, for SEO purposes (keep inbound links). You could create rewrite rule on web server side or add inline rule to URL manager on app side. Example:
'urlManager' => [
'rules' => [
'<controller:RegisterUser>/<action:\w+>'=>'register-user/<action>',
],
],
Also it's possible to write custom URL rule. Example:
namespace app\components;
use yii\web\UrlRuleInterface;
use yii\base\Object;
class CarUrlRule extends Object implements UrlRuleInterface
{
public function createUrl($manager, $route, $params)
{
if ($route === 'car/index') {
if (isset($params['manufacturer'], $params['model'])) {
return $params['manufacturer'] . '/' . $params['model'];
} elseif (isset($params['manufacturer'])) {
return $params['manufacturer'];
}
}
return false; // this rule does not apply
}
public function parseRequest($manager, $request)
{
$pathInfo = $request->getPathInfo();
if (preg_match('%^(\w+)(/(\w+))?$%', $pathInfo, $matches)) {
// check $matches[1] and $matches[3] to see
// if they match a manufacturer and a model in the database
// If so, set $params['manufacturer'] and/or $params['model']
// and return ['car/index', $params]
}
return false; // this rule does not apply
}
}
And use the new rule class in the [[yii\web\UrlManager::rules]] configuration:
[
// ...other rules...
[
'class' => 'app\components\CarUrlRule',
// ...configure other properties...
],
]
You need to specify your action like this ['/site/register-user']. As documentation says about Inline Actions:
index becomes actionIndex, and hello-world becomes actionHelloWorld
In my controller I create the Navigation object and passing it to the view
$navigation = new \Zend\Navigation\Navigation(array(
array(
'label' => 'Album',
'controller' => 'album',
'action' => 'index',
'route' => 'album',
),
));
There trying to use it
<?php echo $this->navigation($this->navigation)->menu() ?>
And get the error:
Fatal error: Zend\Navigation\Exception\DomainException: Zend\Navigation\Page\Mvc::getHref cannot execute as no Zend\Mvc\Router\RouteStackInterface instance is composed in Zend\View\Helper\Navigation\AbstractHelper.php on line 471
But navigation which I use in layout, so as it is written here: http://adam.lundrigan.ca/2012/07/quick-and-dirty-zf2-zend-navigation/ works. What is my mistake?
Thank you.
The problem is a missing Router (or to be more precise, a Zend\Mvc\Router\RouteStackInterface). A route stack is a collection of routes and can use a route name to turn that into an url. Basically it accepts a route name and creates an url for you:
$url = $routeStack->assemble('my/route');
This happens inside the MVC Pages of Zend\Navigation too. The page has a route parameter and when there is a router available, the page assembles it's own url (or in Zend\Navigation terms, an href). If you do not provide the router, it cannot assemble the route and thus throws an exception.
You must inject the router in every page of the navigation:
$navigation = new Navigation($config);
$router = $serviceLocator->get('router');
function injectRouter($navigation, $router) {
foreach ($navigation->getPages() as $page) {
if ($page instanceof MvcPage) {
$page->setRouter($router);
}
if ($page->hasPages()) {
injectRouter($page, $router);
}
}
}
As you see it is a recursive function, injecting the router into every page. Tedious! Therefore there is a factory to do this for you. There are four simple steps to make this happen.
STEP ONE
Put the navigation configuration in your module configuration first. Just as you have a default navigation, you can create a second one secondary.
'navigation' => array(
'secondary' => array(
'page-1' => array(
'label' => 'First page',
'route' => 'route-1'
),
'page-2' => array(
'label' => 'Second page',
'route' => 'route-2'
),
),
),
You have routes to your first page (route-1) and second page (route-2).
STEP TWO
A factory will convert this into a navigation object structure, you need to create a class for that first. Create a file SecondaryNavigationFactory.php in your MyModule/Navigation/Service directory.
namespace MyModule\Navigation\Service;
use Zend\Navigation\Service\DefaultNavigationFactory;
class SecondaryNavigationFactory extends DefaultNavigationFactory
{
protected function getName()
{
return 'secondary';
}
}
See I put the name secondary here, which is the same as your navigation key.
STEP THREE
You must register this factory to the service manager. Then the factory can do it's work and turn the configuration file into a Zend\Navigation object. You can do this in your module.config.php:
'service_manager' => array(
'factories' => array(
'secondary_navigation' => 'MyModule\Navigation\Service\SecondaryNavigationFactory'
),
)
See I made a service secondary_navigation here, where the factory will return a Zend\Navigation instance then. If you do now $sm->get('secondary_navigation') you will see that is a Zend\Navigation\Navigation object.
STEP FOUR
Tell the view helper to use this navigation and not the default one. The navigation view helper accepts a "navigation" parameter where you can state which navigation you want. In this case, the service manager has a service secondary_navigation and that is the one we need.
<?= $this->navigation('secondary_navigation')->menu() ?>
Now you will have the navigation secondary used in this view helper.
Disclosure: this answer is the same as I gave on this question: https://stackoverflow.com/a/12973806/434223
btw. you don't need to define controller and action if you define a route, only if your route is generic and controller/action are variable segments.
The problem is indeed that the routes can't be resolved without the router. I would expect the navigation class to solve that issue, but obviously you have to do it on your own. I just wrote a view helper to introduce the router with the MVC pages.
Here's how I use it within the view:
$navigation = $this->navigation();
$navigation->addPage(
array(
'route' => 'language',
'label' => 'language.list.nav'
)
);
$this->registerNavigationRouter($navigation);
echo $navigation->menu()->render();
The view helper:
<?php
namespace JarJar\View\Helper;
use Zend\View\Helper\AbstractHelper;
use Zend\View\Helper\Navigation;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\Navigation\Page\Mvc;
class RegisterNavigationRouter extends AbstractHelper implements ServiceLocatorAwareInterface
{
protected $serviceLocator;
public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
}
public function getServiceLocator()
{
return $this->serviceLocator;
}
public function __invoke(Navigation $navigation)
{
$router = $this->getRouter();
foreach ($navigation->getPages() as $page) {
if ($page instanceof Mvc) {
$page->setRouter($router);
}
}
}
protected function getRouter()
{
$router = $this->getServiceLocator()->getServiceLocator()->get('router');
return $router;
}
}
Don't forget to add the view helper in your config as invokable instance:
'view_helpers' => array(
'invokables' => array(
'registerNavigationRouter' => 'JarJar\View\Helper\RegisterNavigationRouter'
)
),
It's not a great solution, but it works.