I try to setup a SOAP Server for Laravel Framework 5.5.13. Therefore I have created two classes (Server.php and Client.php) and two Controllers, SoapServerController and SoapClientController.
Here's the source code:
app/Classes/Soap/Server.php
namespace App\Classes\Soap;
class Server {
public function __construct() {
}
public function getDate() {
return date('Y-m-d');
}
}
app/Classes/Soap/Client.php
namespace App\Classes\Soap;
class Client {
protected $instance;
public function __construct() {
$params = array( 'uri' => '/soap/server',
'location' => url('/soap/server'),
'trace' => 1,
'soap_version' => SOAP_1_2
);
$this->instance = new \SoapClient( null, $params );
}
public function getDate() {
return $this->instance->getDate();
}
}
app/Http/Controllers/SoapServerController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Classes\Soap;
class SoapServerController extends Controller
{
public function index() {
$params = array( 'uri' => url('/soap/server') );
$server = new \SoapServer( null, $params );
$server->setClass( Soap\Server::class );
$server->handle();
}
}
app/Http/Controllers/SoapClientController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Classes\Soap;
class SoapClientController extends Controller
{
public function index() {
$client = new Soap\Client;
$client->getDate();
}
}
When I open the route /api/soap/client, I get the error:
SoapFault: Method Not Allowed
Do I have to change something in my routes file?
It's important to note that SOAP calls are all supposed to be performed with POST requests. Most likely, you have your route set up with only GET requests, hence the method (POST) is not allowed.
Related
I'm attempting to create and implement my own authentication and it keeps trying to reach Users.login.
Since I specify which authenticate class to use, implement my own authenticate() method, why does it keep attempting to load it?
Full error:
Missing Method in UsersController
Cake\Controller\Exception\MissingActionException
Documentation API
The action login is not defined in UsersController
Error: Create UsersController::login() in file: src\Controller\UsersController.php.
AppController
public function initialize()
{
...
$this->loadComponent('Auth');
$this->Auth->config('authenticate', ['Sso']);
...
}
src/Auth/SsoAuthenticate.php
<?php
namespace App\Auth;
use Cake\Auth\BaseAuthenticate;
use Cake\Http\ServerRequest;
use Cake\Http\Response;
use Cake\ORM\TableRegistry;
use Cake\Log\Log;
class SsoAuthenticate extends BaseAuthenticate
{
public function authenticate(ServerRequest $request, Response $response)
{
Log::debug('SSO Authenticate()');
//hard coded for testing
return false;
}
}
?>
I had been initially following the lead of the following post: CakePHP 3 Ldap authentication issue and clarification
Edit #1
Swapped to returning a valid user and I'm still getting the same results. With every change, I'm using a private window to ensure no session exists, etc.
public function authenticate(ServerRequest $request, Response $response)
{
Log::debug('SSO Authenticate()');
debug('SSO Authenticate()');
$table = TableRegistry::get('Users');
$u = $table->get(1);
debug($u);
return $u;
}
Edit #2
Having debug statements, error throwing or even exit will not stop this from going through. This must be a configuration issue? Caching?
public function authenticate(ServerRequest $request, Response $response)
{
Log::debug('SSO Authenticate()');
debug('SSO Authenticate()');
throw new NotFoundException(__('Article not found'));
exit;
}
Output of debug($this->Auth)
object(Cake\Controller\Component\AuthComponent) {
'components' => [
(int) 0 => 'RequestHandler',
(int) 1 => 'Flash'
],
'implementedEvents' => [
'Controller.initialize' => 'authCheck',
'Controller.startup' => 'startup'
],
'_config' => [
'authenticate' => [
(int) 0 => 'Sso'
],
'authorize' => null,
'ajaxLogin' => null,
'flash' => null,
'loginAction' => null,
'loginRedirect' => null,
'logoutRedirect' => null,
'authError' => null,
'unauthorizedRedirect' => true,
'storage' => 'Session',
'checkAuthIn' => 'Controller.startup'
]
}
Edit #3
Seems this goes through at the moment, even though I'm throwing an error and existing in authenticate.
<?php
namespace App\Auth;
use Cake\Auth\BaseAuthenticate;
use Cake\Http\ServerRequest;
use Cake\Http\Response;
use Cake\ORM\TableRegistry;
use Cake\Log\Log;
class SsoAuthenticate extends BaseAuthenticate
{
public function authenticate(ServerRequest $request, Response $response)
{
Log::debug('SSO Authenticate()');
debug('SSO Authenticate()');
throw new NotFoundException(__('Article not found'));
exit;
}
public function getUser(ServerRequest $request)
{
$table = TableRegistry::get('Users');
return $table->get(1)->toArray();
}
}
?>
Edit #4
Started from a blank app, implemented the following. Would it be acceptable to throw an error like that if my SSO headers aren't present?
src/Controller/AppController.php
public function initialize()
{
parent::initialize();
$this->loadComponent('RequestHandler', [
'enableBeforeRedirect' => false,
]);
$this->loadComponent('Flash');
$this->loadComponent('Auth');
$this->Auth->config('authenticate', ['Sso']);
}
src/Auth/SsoAuthenticate.php
<?php
namespace App\Auth;
use Cake\Auth\BaseAuthenticate;
use Cake\Http\ServerRequest;
use Cake\Http\Response;
use Cake\ORM\TableRegistry;
use Cake\Log\Log;
use Cake\Core\Exception\Exception;
class SsoAuthenticate extends BaseAuthenticate
{
public function getUser(ServerRequest $request)
{
//return true; //looks for Users.login
//return false; //looks for Users.login
//return ['name' => 'Dan']; //works!
$noHeaders = false; //will add appropriate logic here
if($noHeaders) //hard stop!
throw new Exception("No headers configured!");
else //insert or update user based on headers and what's in the current db
return ['name' => "Myself"]; //
}
public function authenticate(ServerRequest $request, Response $response){}
}
?>
Basically, my question is; Why does the $this->app->instance() Call work on one instance of the mocked object, but the other doesn't...
In the example below, the getGroupSingleSignOnLink function actually gets called, the other is mocked and the test passes...
TEST
namespace Tests\Feature;
use App\Models\Group;
use App\Models\User;
use Tests\TestCase;
use App\Clients\SingleSignOnApi;
use Mockery;
class SingleSignOnTest extends TestCase
{
private $validUrl = 'http://www.google.com';
public function setUp()
{
parent::setUp();
$single_sign_on = Mockery::mock(SingleSignOnApi::class);
$single_sign_on->shouldReceive('getGroupSingleSignOnLink')->andReturn($this->validUrl);
$single_sign_on->shouldReceive('getSingleSignOnLink')->andReturn($this->validUrl);
$this->app->instance(SingleSignOnApi::class, $single_sign_on);
}
//THIS TEST FAILS, SingleSignOnApi Class Not Mocked
public function testGroupAuthConnection()
{
$group = Group::whereNotNull('external_platform_key')->first();
$user = $group->users()->first();
$this->be($user);
$group_sso = $group->groupAuthConnections()->first();
$response = $this->get(route('sso.group.connect', ['id' => $group_sso->id]));
$response->assertRedirect($this->validUrl);
$response->assertSessionMissing('__danger');
}
//THIS TEST PASSES, The SingleSignOnApi Class is Mocked
public function testAuthConnectionConnect()
{
$user = User::first();
$this->be($user);
$sso = $user->authConnections()->firstOrFail();
$response = $this->get(route('sso.connect', ['id' => $sso->id]));
$response->assertRedirect($this->validUrl);
$response->assertSessionMissing('__danger');
}
}
CONTROLLER FUNC - TEST MOCK WORKING
public function connect($id)
{
$auth_connection = $this->findAuthConnection($id, Auth::user());
$sso_client = App::make(SingleSignOnApi::class);
$url = $sso_client->getSingleSignOnLink($auth_connection);
return redirect($url);
}
CONTROLLER FUNC - TEST MOCK NOT WORKING
public function connect($id)
{
$group_ids = Auth::user()->groups()->pluck('groups.id')->toArray();
$group_auth_connection = $this->findGroupAuthConnection($id, Auth::user());
//This is the Mocked Object in my Test: SingleSignOnApi
$sso_client = App::make(SingleSignOnApi::class, [$group_auth_connection->group->external_platform_key]);
$url = $sso_client->getGroupSingleSignOnLink($group_auth_connection, Auth::user());
return redirect($url);
}
I'll use Quickbooks as an example to illustrate how I got this to work consistently for me.
Here's my AppServiceProvider, defining a custom QuickbooksSDK Class I created:
...
public function boot()
{
$this->app->bind(QuickbooksSDK::class, function($app) {
return new QuickbooksSDK(
new GuzzleHttp\Client([
'base_uri' => config('invoicing.baseUri'),
'timeout' => 3,
'headers' => [
'Authorization' => 'Bearer '.config('invoicing.apiKey'),
'Accept' => 'application/json'
],
'http_errors' => false
]),
config('invoicing.apiKey'),
env('QUICKBOOKS_REFRESH_TOKEN'),
env('QUICKBOOKS_CLIENT_ID'),
env('QUICKBOOKS_CLIENT_SECRET')
);
});
}
Then I created a second custom class, called the QuickbooksInvoicingDriver, that takes the instantiated SDK Class from the Service container:
public function __construct()
{
$this->api = app(QuickbooksSDK::class);
}
Finally, in my test class, I can mock the QuickbooksSDK, with my own custom responses, to make testing easier:
$vendorResponse = '{"Vendor": {"Balance": 0,"Vendor1099": false,"CurrencyRef": {"value": "GYD","name": "Guyana Dollar"},"domain": "QBO","sparse": false,"Id": "25","SyncToken": "0","MetaData": {"CreateTime": "2018-04-04T12:36:47-07:00","LastUpdatedTime": "2018-04-04T12:36:47-07:00"},"DisplayName": "daraul","PrintOnCheckName": "daraul","Active": true},"time": "2018-04-04T12:36:47.576-07:00"}';
$mock = new MockHandler([
new Response(200, [], $vendorResponse),
]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
$api = new QuickbooksSDK(
$client,
'test',
'test',
'test',
'test'
);
$this->app->instance(QuickbooksSDK::class, $api);
Now I can run my tests normally, without worrying about third parties. These links were really helpful for me:
http://docs.guzzlephp.org/en/stable/testing.html
https://laravel.com/docs/5.5/container
Let me know if this was helpful.
I am quite new to ZF2 and I am preparing a demo application with simple login and CRUD system. Now for login I have prepared a plugin which consists of some functions that will authenticate users, return the logged in user data, return the logged in status etc. But the problem that I am facing is I can't initialize any variable into the constructor of my controller which will store any return value from the plugin. It's always showing service not found exception.
Please find my plugin code below:
AuthenticationPlugin.php
<?php
namespace Album\Controller\Plugin;
use Zend\Mvc\Controller\Plugin\AbstractPlugin;
use Zend\Session\Container as SessionContainer;
use Zend\View\Model\ViewModel;
use Album\Entity\User;
class AuthenticationPlugin extends AbstractPlugin{
protected $entityManager;
protected $usersession;
public function __construct(){
$this->usersession = new SessionContainer('UserSession');
}
public function dologin($email,$password)
{
$getData = $this->em()->getRepository('Album\Entity\User')->findOneBy(array('email' => $email, 'password' => $password));
if(count($getData)){
$this->usersession->offsetSet('userid', $getData->getId());
return true;
}
else{
return false;
}
}
public function isloggedin(){
$userid = $this->usersession->offsetGet('userid');
if(!empty($userid)){
return true;
}
else{
return false;
}
}
public function logindata(){
$userid = $this->usersession->offsetGet('userid');
$getData = $this->em()->getRepository('Album\Entity\User')->findOneBy(array('id' => $userid));
return $getData;
}
public function logout(){
$this->usersession->offsetUnset('userid');
}
public function em(){
return $this->entityManager = $this->getController()->getServiceLocator()->get('Doctrine\ORM\EntityManager');
}
}
?>
In my module.config.php
'controller_plugins' => array(
'invokables' => array(
'AuthPlugin' => 'Album\Controller\Plugin\AuthenticationPlugin',
)
),
Now I am doing this in my controller:
protected $entityManager;
protected $isloggedin;
protected $authentication;
public function __construct(){
$this->authentication = $this->AuthPlugin();
$this->isloggedin = $this->authentication->isloggedin();
}
The error I am getting is like below:
An error occurred An error occurred during execution; please try again
later. Additional information:
Zend\ServiceManager\Exception\ServiceNotFoundException
File:
D:\xampp\htdocs\subhasis\zf2-tutorial\vendor\zendframework\zendframework\library\Zend\ServiceManager\ServiceManager.php:555
Message:
Zend\Mvc\Controller\PluginManager::get was unable to fetch or create an instance for AuthPlugin
But if I write the above constructor code in any of my controller actions everything is fine. in ZF1 I could initialize any variable in the init() method and could use the variable in any of my actions. How can I do this in ZF2? Here, I want to detect if the user is logged in the constructor itself. Now I have to call the plugin in every action which I don't want.
What should I do here?
The error you are receiving is because you are trying to use the ServiceManager (via the Zend\Mvc\Controller\PluginManager) in the __construct method of the controller.
When a controller is registered as an invokable class, the Service Manager (ControllerManager) is responsible for the creating the controller instance. Once created, it will then call the controllers various default 'initializers' which also inlcudes the plugin manager. By having your code in __construct it is trying to use the plugin manager before it has been set.
You can resolve this by using a controller factory, rather than an invokable in module.config.php.
'controllers' => [
'factories' => [
'MyModule\Controller\Foo' => 'MyModule\Controller\FooControllerFactory',
],
],
Then the factory
namespace MyModule\Controller\FooControllerFactory;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class FooControllerFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $controllerManager)
{
$serviceManager = $controllerManager->getServiceLocator();
$controllerPluginManager = $serviceManager->get('ControllerPluginManager');
$authPlugin = $controllerPluginManager->get('AuthPlugin');
return new FooController($authPlugin);
}
}
Lastly, update the controller __construct to add the new argument and remove the call to $this->authPlugin()
class FooController extends AbstractActionController
{
public function __construct(AuthPlugin $authentication)
{
$this->authentication = $authentication;
$this->isloggedin = $authentication->isloggedin();
}
}
In two words: I need to get access to the service manager (locator) from external class.
Details:
I have next structure in my ZF2 project:
Api.php is the class, I use in SOAP server, which is created in Controller:
class IncomingInterfaceController extends AbstractActionController
{
...
public function indexAction()
{
if (isset($_GET['wsdl']))
$this->handleWSDL();
else
$this->handleSOAP();
return $this->getResponse();
}
private function handleWSDL()
{
$autodiscover = new AutoDiscover();
$autodiscover->setClass('\Application\Api\Api')->setUri($this->getURI());
$autodiscover->handle();
}
In this Api.php class I need to get access to services.
I need something like this in my Api.php class:
public function OnNewDeal($uid)
{
$error_log=$this->getServiceLocator()->get('error_log'); // this doesn't work!
$error_log->write_error('error_text');
}
In Module.php
public function getServiceConfig() {
return array(
'invokables' => array(
'Application\Api\Api' => 'Application\Api\Api'
)
);
}
In Api.php
class Api implements ServiceLocatorAwareInterface{
protected $services;
public function OnNewDeal($uid){
$this->getServiceLocator()->get('error_log')->write_error('SOAP ERROR');
}
public function setServiceLocator(ServiceLocatorInterface $serviceLocator){
$this->services = $serviceLocator;
}
public function getServiceLocator(){
return $this->services;
}
}
In IncomingInterfaceController.php
class IncomingInterfaceController extends AbstractActionController{
...
protected $api;
public function indexAction()
{
if (isset($_GET['wsdl']))
$this->handleWSDL();
else
$this->handleSOAP();
return $this->getResponse();
}
private function handleWSDL()
{
$autodiscover = new AutoDiscover();
$autodiscover->setClass('\Application\Api\Api')->setUri($this->getURI());
$autodiscover->handle();
}
public getApi(){
if(!$api){
$this->api = $this->getServiceLocator()->get('Application\Api\Api');
}
return $this->api;
}
In controller where you do $this->handleSOAP(); use setObject with already created instance instead setClass.
You should pass into Api __construct $this->getServiceLocator() and handle it there.
class IncomingInterfaceController extends AbstractActionController
{
private function handleSOAP()
{
$soap = new Server(null, array('wsdl'=>$this->getWSDLURI()));
$soapClass = new \Application\Api\Api($this->getServiceLocator());
$soap->setObject($soapClass);
$soap->handle();
}
In Api class, handle serviceManager instance and use as you wish:
class Api
{
protected $serviceManager;
public function __construct($serviceManager)
{
$this->serviceManager = $serviceManager;
}
public function OnNewDeal($uid)
{
$this->serviceManager->get('error_log')->write_error('SOAP ERROR');
}
....
}
Perhaps your API could implement ServiceLocatorAwareInterface like:
class Api implements ServiceLocatorAwareInterface
and add
class Api implements ServiceLocatorAwareInterface
{
protected $serviceManager;
}
Then the service manager would be available
UPDATED
module.config.php example
<?php
return array(
'service_manager' => array(
'factories' => array(
'Api' => 'Namespace\Api'
),
'shared' => array(
'Api' => false
)
),
)
?>
Injecting the Service Manager instance to an user defined "service locator aware class" should responsibility of the framework's itself (via initializers, invokables or user defined factories) not a specific controller's handleSOAP() method.
Yes, #SirJ's solution will work too but that's not a good practice. ZF2 provides ready-to-use Traits and Interfaces exactly for requirements like this. Just use them!
Your very own API class should seem like this:
<?php
namespace Application\Api;
use Zend\ServiceManager\ServiceLocatorInterface;
class Api implements ServiceLocatorInterface
{
// Here is the trait. (php >= 5.4)
use \Zend\ServiceManager\ServiceLocatorAwareTrait;
public function OnNewDeal($uid)
{
$this->getServiceLocator()->get('error_log')->write_error('SOAP ERROR');
}
}
And you should add this key to your module.config.php
<?php
return array(
'service_manager' => array(
'invokables' => array(
'api-service' => 'Application\Api\Api',
)
);
Thats all! Now you can:
<?php
...
$soap = new Server(null, array('wsdl'=>$this->getWSDLURI()));
$soapClass = $this->getServiceLocator()->get('api-service');
$soap->setObject($soapClass);
...
I want to call the method SteeringWheelMapper->fetchCarBrandList() from a controller.
This works good now, but there's a problem.
The SteeringWheelMapper extends the AbstractWebServiceMapper which has a construct method which requires an instance of \Zend\Http\Client.
As you can see in my module.config.php file, I use "factories" for the instantiation of my SteeringWheelMapper.
The supplier has multiple products, so I will have to build multiple mappers. In the current situation that means I have to add a key to the factories config for every mapper which extends AbstractWebServiceMapper.
For example, when I want to add an ExhaustMapper, I have to add
SupplierName\Mapper\Exhaust => function ($serviceMapper) {
$httpClient => new \Zend\Http\Client;
return new SupplierName\Mapper\ExhaustMapper($httpClient);
}
Now I am repeating myself, because I also have to do this for SupplierName\Mapper\SteeringWheelMapper.
I think there should be a way to make a factory for all the mappers, instead of a new key added to the factories config.
Is my thought right?
Does anyone has a suggestion how I should do this?
Please see code below.
I'm using ZF2 and I use this setup:
/vendor
SupplierName
config
module.config.php
log
log.log
src
SupplierName
Entity
AbstractEntity.php
SteeringWheelEntity.php
Mapper
AbstractWebServiceMapper.php
SteeringWheelMapper.php
$steeringWheelMapper = $this->getServiceLocator()->get('SupplierName\Mapper\SteeringWheel');
$carBrandList = $steeringWheelMapper->fetchCarBrandsList();
SteeringWheelMapper.php
<?php
namespace SupplierName\Mapper;
class SteeringWheelMapper extends AbstractWebServiceMapper
{
public function fetchCarBrandList()
{
// Code for request
// Dispatch HTTP request
$this->dispatch();
}
}
My SupplierName/config/module.config.php looks like this:
<?php
return array(
'service_manager' => array(
'factories' => array(
'SupplierName\Mapper\SteeringWheel' => function ($serviceManager) {
$httpClient = new \Zend\Http\Client;
return new SupplierName\Mapper\SteeringWheelMapper($httpClient);
},
),
),
'supplier_name' => array(
'api' => array(
'url' => 'http://api.example.com',
),
'log' => array(
'file_location' => __DIR__ . '/../log/log.log',
),
),
);
What you're actually talking about is an abstract factory, the service manager supports the concept, but you'll need to write your own, here's an example that assumes all your mappers begin with SupplierName\Mapper
<?php
namespace SupplierName\Services;
use Zend\ServiceManager\AbstractFactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class MapperAbstractFactory implements AbstractFactoryInterface
{
public function canCreateServiceWithName(ServiceLocatorInterface $locator, $name, $requestedName)
{
if (0 === strpos($requestedName, 'SupplierName\\Mapper') && class_exists($requestedName)){
return true;
}
return false;
}
public function createServiceWithName(ServiceLocatorInterface $locator, $name, $requestedName)
{
$httpClient = new \Zend\Http\Client;
return new $requestedName($httpClient);
}
}
In your service config, add an abstract factories key, along with the fqcn of the abstract factory, and hopefully any time you call $sm->get('SupplierName\Mapper\SomeClass'); providing the class exists, you'll get a composed instance returned
public function getServiceConfig()
{
return array(
'abstract_factories' => array(
'SupplierName\Services\MapperAbstractFactory'
),
);
}
Final working solution:
<?php
// module/Application/src/Application/Controller/IndexController.php
namespace Application\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Zend\I18n\Translator\Translator;
class IndexController extends AbstractActionController
{
protected $translator;
public function __construct(Translator $translator)
{
$this->translator = $translator;
}
public function indexAction()
{
$steeringWheelMapper = $this->getServiceLocator()->get('SupplierName\Mapper\SteeringWheel');
$carBrandList = $steeringWheelMapper->fetchCarBrandList();
return new ViewModel();
}
}
<?php
// vendor/SupplierName/src/SupplierName/Module.php
namespace SupplierName;
class Module
{
public function getConfig()
{
return include __DIR__ . '/../../config/module.config.php';
}
public function getServiceConfig()
{
return array(
'abstract_factories' => array(
'SupplierName\Mapper\MapperAbstractFactory'
),
);
}
public function getAutoloaderConfig()
{
return array(
'Zend\Loader\StandardAutoloader' => array(
'namespaces' => array(
__NAMESPACE__ => __DIR__,
),
),
);
}
}
<?php
// vendor/SupplierName/src/SupplierName/Mapper/SteeringWheelMapper.php
namespace SupplierName\Mapper;
class SteeringWheelMapper extends AbstractWebServiceMapper
{
public function fetchCarBrandList()
{
$this->dispatch();
}
}
<?php
// vendor/SupplierName/src/SupplierName/Mapper/AbstractWebServiceMapper.php
namespace SupplierName\Mapper;
use \Zend\Http\Client;
class AbstractWebServiceMapper
{
public function __construct(Client $client)
{
}
public function dispatch()
{
}
}
<?php
// vendor/SupplierName/src/SupplierName/Mapper/MapperAbstractFactory.php
namespace SupplierName\Mapper;
use Zend\ServiceManager\AbstractFactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use \Zend\Http\Client;
class MapperAbstractFactory implements AbstractFactoryInterface
{
public function canCreateServiceWithName(ServiceLocatorInterface $locator, $name, $requestedName)
{
if (0 === strpos($requestedName, 'SupplierName\Mapper')) {
$requestedName .= 'Mapper';
if (class_exists($requestedName)) {
return true;
}
}
return false;
}
public function createServiceWithName(ServiceLocatorInterface $locator, $name, $requestedName)
{
$requestedName .= 'Mapper';
$httpClient = new Client();
return new $requestedName($httpClient);
}
}