I've built a first-run web service on Zend Framework (1.10), and now I'm looking at ways to refactor some of the logic in my Action Controllers so that it will be easier for me and the rest of my team to expand and maintain the service.
I can see where there are opportunities for refactoring, but I'm not clear on the best strategies on how. The best documentation and tutorials on controllers only talk about small scale applications, and don't really discuss how to abstract the more repetitive code that creeps into larger scales.
The basic structure for our action controllers are:
Extract XML message from the request body - This includes validation against an action-specific relaxNG schema
Prepare the XML response
Validate the data in the request message (invalid data throws an exception - a message is added to the response which is sent immediately)
Perform database action (select/insert/update/delete)
Return success or failure of action, with required information
A simple example is this action which returns a list of vendors based on a flexible set of criteria:
class Api_VendorController extends Lib_Controller_Action
{
public function getDetailsAction()
{
try {
$request = new Lib_XML_Request('1.0');
$request->load($this->getRequest()->getRawBody(), dirname(__FILE__) . '/../resources/xml/relaxng/vendor/getDetails.xml');
} catch (Lib_XML_Request_Exception $e) {
// Log exception, if logger available
if ($log = $this->getLog()) {
$log->warn('API/Vendor/getDetails: Error validating incoming request message', $e);
}
// Elevate as general error
throw new Zend_Controller_Action_Exception($e->getMessage(), 400);
}
$response = new Lib_XML_Response('API/vendor/getDetails');
try {
$criteria = array();
$fields = $request->getElementsByTagName('field');
for ($i = 0; $i < $fields->length; $i++) {
$name = trim($fields->item($i)->attributes->getNamedItem('name')->nodeValue);
if (!isset($criteria[$name])) {
$criteria[$name] = array();
}
$criteria[$name][] = trim($fields->item($i)->childNodes->item(0)->nodeValue);
}
$vendors = $this->_mappers['vendor']->find($criteria);
if (count($vendors) < 1) {
throw new Api_VendorController_Exception('Could not find any vendors matching your criteria');
}
$response->append('success');
foreach ($vendors as $vendor) {
$v = $vendor->toArray();
$response->append('vendor', $v);
}
} catch (Api_VendorController_Exception $e) {
// Send failure message
$error = $response->append('error');
$response->appendChild($error, 'message', $e->getMessage());
// Log exception, if logger available
if ($log = $this->getLog()) {
$log->warn('API/Account/GetDetails: ' . $e->getMessage(), $e);
}
}
echo $response->save();
}
}
So - knowing where the commonalities are in my controllers, what's the best strategy for refactoring while keeping it Zend-like and also testable with PHPUnit?
I did think about abstracting more of the controller logic into a parent class (Lib_Controller_Action), but this makes unit testing more complicated in a way that seems to me to be wrong.
Two ideas (just creating an answer from the comments above):
Push commonality down into service/repository classes? Such classes would be testable, would be usable across controllers, and could make controller code more compact.
Gather commonality into action helpers.
Since you have to do this step every time a request is made, you could store that receive, parse and validate the received request in a Zend_Controller_Plugin which would be run every PreDispatch of all controllers. (Only do-able if your XML request are standardized) (If you use XMLRPC, REST or some standard way to build requests to your service, you could look forward those modules built in ZF)
The validation of the data (action specific) could be done in a controller method (which would then be called by the action(s) needing it) (if your parametters are specific to one or many actions of that controller) or you could do it with the patterns Factory and Builder in the case that you have a lot of shared params between controllers/actions
// call to the factory
$filteredRequest = My_Param_Factory::factory($controller, $action, $paramsArray) // call the right builder based on the action/controller combination
// the actual Factory
class My_Param_Factory
{
public static function factory($controller, $action, $params)
{
$builderClass = "My_Param_Builder_" . ucfirst($controller) . '_' . ucfirst($action);
$builder = new $builderClass($params);
return $builder->build();
}
}
Your builder would then call specific parameters validating classes based on that specific builder needs (which would improve re-usability)
In your controller, if every required params are valid, you pass the processing to the right method of the right model
$userModel->getUserInfo($id) // for example
Which would remove all of the dataprocessing operations from the controllers which would only have to check if input is ok and then dispatch accordingly.
Store the results (error or succes) in a variable that will be sent to the view
Process the data (format and escape (replace < with < if they are to be included in the response for example)), send to a view helper to build XML then print (echo) the data in the view (which will be the response for your user).
public function getDetailsAction()
{
if ($areRequestParamsValid === true) {
// process data
} else {
// Build specific error message (or call action helper or controller method if error is general)
}
$this->view->data = $result
}
Related
So I have a complicated onboarding process that does several steps. I created a class that handles the process but I've added a few more steps and I'd like to refactor this into something a bit more manageable. I refactored to use Laravel's pipeline, but feel this may not be the best refactor due to the output needing to be modified before each step.
Here is an example before and after with some pseudo code.
before
class OnboardingClass {
public $user;
public $conversation;
public function create($firstName, $lastName, $email){
// Step 1
$user = User::create();
// Step 2
$conversation = Conversation::create(); // store information for new user + existing user
// Step 3
$conversation->messages()->create(); // store a message on the conversation
// Step 4
// Send api request to analytics
// Step 5
// Send api request to other service
return $this;
}
}
after
class OnboardingClass{
public $user;
public $conversation;
public function create($firstName, $lastName, $email){
$data = ['first_name' => $firstName, ...]; // form data
$pipeline = app(Pipeline::Class);
$pipeline->send($data)
->through([
CreateUser::class,
CreateNewUserConversation::class,
AddWelcomeMessageToConversation::class,
...
])->then(function($data){
// set all properties returned from last class in pipeline.
$this->user = $data['user'];
$this->conversation = $data['conversation'];
});
return $this;
}
}
Now within each class I modify the previous data and output a modified version something like this
class CreateUser implements Pipe {
public function handle($data, Closure $next) {
// do some stuff
$user = User::create():
return $next([
'user' => $user,
'other' => 'something else'
]);
}
}
In my controller I am simply calling the create method.
class someController() {
public function store($request){
$onboarding = app(OnboardingClass::class);
$onboarding->create('John', 'Doe', 'john#example.com');
}
}
So the first pipe receives the raw form fields and outputs what the second pipe needs to get the job done in its class, then the next class outputs the data required by the next class, so on and so forth. The data that comes into each pipe is not the same each time and you cannot modify the order.
Feels a bit weird and I'm sure there is a cleaner way to handle this.
Any design pattern I can utilize to clean this up a bit?
I think you could try using Laravel Service Provider, for example, you could build a login service provider; or Event & Listener, for example, you could build an listener for login and triggers a event to handle all the necessary logics. Can't really tell which one is the best since outcome is the same and it makes same amount of network requests, but it's more on personal preferences
I have written some functions using Authorize.Net's PHP SDK's that look like the following:
public function getCustomerProfiles() {
$customerProfiles = array();
// Before we can get customer profiles, we need to get a list of all customer id's.
$customerIdListRequest = new AnetAPI\GetCustomerProfileIdsRequest();
$customerIdListRequest->setMerchantAuthentication(self::getMerchantAuth(Config::LOGIN_ID, Config::TRANSACTION_KEY));
$customerIdListController = new AnetController\GetCustomerProfileIdsController($customerIdListRequest);
$customerIdListResponse = $customerIdListController->executeWithApiResponse(\net\authorize\api\constants\ANetEnvironment::SANDBOX);
if(($customerIdListResponse != null) && ($customerIdListResponse->getMessages()->getResultCode() == "Ok")) {
// TODO: Investigate warning about no method named getIds().
foreach( $customerIdListResponse->getIds() as $id ) {
// Now we can get each customer profile.
$request = new AnetAPI\GetCustomerProfileRequest();
$request->setMerchantAuthentication(self::getMerchantAuth(Config::LOGIN_ID, Config::TRANSACTION_KEY));
$request->setCustomerProfileId($id);
$controller = new AnetController\GetCustomerProfileController($request);
$response = $controller->executeWithApiResponse(\net\authorize\api\constants\ANetEnvironment::SANDBOX);
if(($response != null) && ($response->getMessages()->getResultCode() == "Ok")) {
// TODO: Investigate warning about no method named getProfile()
// Add it to the array.
array_push($customerProfiles, $response->getProfile()->xml);
} else {
throw new \Exception($response->getMessages()->getMessage());
}
}
} else {
throw new \Exception($customerIdListResponse->getMessages()->getMessage());
}
return $customerProfiles;
}
Currently, I'm just returning an array of objects. I'd prefer to get the raw XML response. Is this functionality available via Authorize.Net's PHP SDK? Or am I better of using something like Guzzle and making the request manually?
Looking at the source code I think it would be simple enough.
Look the execute method that is invoked by executeWithApiResponse there. See xmlResponse? Just need to store that as a class property (and add a public getter), or maybe tweak the function to take an extra argument telling it to return the raw response. Could hack it, or better yet, extend that ApiOperationBase class (note the interface IApiOperation gives you a outline to follow).
Seeing that serializer also...
$this->apiResponse = $this->serializer->deserialize( $xmlResponse, $this->apiResponseType , 'xml');
Could maybe do something more elegant with that. But not as clear as path I first described.
I'm using Slim Framework version 3.
I've followed the tutorial on creating an app. Part of this involves setting up a classes directory where you can put your own PHP classes.
What I can't understand is how you can access Slim inside those. For example if I have a class src/classes/Users.php and wanted to use the Slim Response code e.g.
return $response->withStatus(302)->withHeader('Location', 'login');
Obviously, $response, is not accessible at that point. It only seems to be in index.php where each callback recieves it as an argument.
Do I have to pass something to every constructor of my own classes, or use or require statements in my classes?
I'd say when domain layer components need to access application level components - this is a code smell. So, consider doing things otherwise, request object describes request. Request contains some data, and that data should be passed to your User class, not request object itself.
If you still wish to use Request object in Users class, simply pass it as argument, like this:
// in your routes
$app->post('users', function($request, $response) {
$user = new User;
$user->hydrateAndPersist($request); // there you pass it as argument
});
// in your user class
class User
{
public function hydrateAndPersist(
\Psr\Http\Message\ServerRequestInterface $request,
\Psr\Http\Message\ResponseInterface $response // yes, you force yourself into injecting response object
) {
// request is here, let's extract data from it
$submittedData = $request->getParams();
// terrible indeed, but this is just an example:
foreach ($submittedData as $prop => $value) {
$this->prop = $value;
}
$result = $this->save();
return $response->withJson($result);
}
}
However, in this case your User class is coupled heavily with PSR-7 request and response objects. Sometimes coupling is not a problem, but in your case User class belongs to domain layer (since it describes User entity), while $request and $response are components of application layer.
Try to reduce coupling:
$app->post('users', function($request, $response) {
$submittedData = $request->getParams();
$user = new User;
$result = $user->hydrateAndPersist($submittedData);
// response is already declared in this scope, let's "put" result of the operation into it
$response = $response->withJson($result);
return $response;
});
class User
{
public function hydrateAndPersist(array $data) : bool
{
$result = false;
foreach ($submittedData as $prop => $value) {
$this->prop = $value;
}
$result = $this->save();
return $result;
}
}
See the benefit? User::hydrateAndPersist now accepts array as argument, it has no knowledge of $request and $response. So, it is not tied to HTTP (PSR-7 describes HTTP messages), it can work with anything. Classes separated, layers separated, ease of maintenance.
To sum up: you can access $request object in your User class by simply passing $request to one of User methods. However, this is poor design that will reduce maintainability of your code.
How do I get started with mocking a web service in PHP? I'm currently directly querying the web API's in my unit testing class but it takes too long. Someone told me that you should just mock the service. But how do I go about that? I'm currently using PHPUnit.
What I have in mind is to simply save a static result (json or xml file) somewhere in the file system and write a class which reads from that file. Is that how mocking works? Can you point me out to resources which could help me with this. Is PHPUnit enough or do I need other tools? If PHPUnit is enough what part of PHPUnit do I need to check out? Thanks in advance!
You would mock the web service and then test what is returned. The hard coded data you are expecting back is correct, you set the Mock to return it, so then additional methods of your class may continue to work with the results. You may need Dependency Injection as well to help with the testing.
class WebService {
private $svc;
// Constructor Injection, pass the WebService object here
public function __construct($Service = NULL)
{
if(! is_null($Service) )
{
if($Service instanceof WebService)
{
$this->SetIWebService($Service);
}
}
}
function SetWebService(WebService $Service)
{
$this->svc = $Service
}
function DoWeb($Request)
{
$svc = $this->svc;
$Result = $svc->getResult($Request);
if ($Result->success == false)
$Result->Error = $this->GetErrorCode($Result->errorCode);
}
function GetErrorCode($errorCode) {
// do stuff
}
}
Test:
class WebServiceTest extends PHPUnit_Framework_TestCase
{
// Simple test for GetErrorCode to work Properly
public function testGetErrorCode()
{
$TestClass = new WebService();
$this->assertEquals('One', $TestClass->GetErrorCode(1));
$this->assertEquals('Two', $TestClass->GetErrorCode(2));
}
// Could also use dataProvider to send different returnValues, and then check with Asserts.
public function testDoWebSericeCall()
{
// Create a mock for the WebService class,
// only mock the getResult() method.
$MockService = $this->getMock('WebService', array('getResult'));
// Set up the expectation for the getResult() method
$MockService->expects($this->any())
->method('getResult')
->will($this->returnValue(1)); // Change returnValue to your hard coded results
// Create Test Object - Pass our Mock as the service
$TestClass = new WebService($MockService);
// Or
// $TestClass = new WebService();
// $TestClass->SetWebServices($MockService);
// Test DoWeb
$WebString = 'Some String since we did not specify it to the Mock'; // Could be checked with the Mock functions
$this->assertEquals('One', $TestClass->DoWeb($WebString));
}
}
This mock may then be used in the other functions since the return is hard coded, your normal code would process the results and perform what work the code should (Format for display, etc...). This could also then have tests written for it.
I'm creating a service to fetch some user data
class ExampleService{
// ...
public function getValueByUser($user)
{
$result = $this->em->getRepository('SomeBundle:SomeEntity')->getValue($user);
if (!$result instanceof Entity\SomeEntity) {
throw new Exception\InvalidArgumentException("no value found for that user");
}
return $result;
}
}
Then in my controller I have
// ...
$ExampleService = $this->get('example_serivce');
$value = $ExampleService->getValueByUser($user);
Should I be using an exception here to indicate that no value was found for that user in the database?
If I should, how do I handle what is returned from $ExampleService->getValueByUser($user) in the controller - let's say I just want to set a default value if nothing is found (or exception returned)
Here is how I do it. Let's use a user service and a controller as an example. It's not an exceptional condition in the service layer — it just returns the result without checking it:
class UserService
{
public function find($id)
{
return $this->em->getRepository('UserBundle:User')->find($id);
}
}
But in the controllers layer I throw an exception if the requested user not found:
class UserController
{
public function viewAction($id)
{
$user = $this->get('user.service')->find($id);
if (!$user) {
throw $this->createNotFoundException(
$this->get('translator')->trans('user.not_found')
);
}
// ...
}
}
Where you want to handle the exception is kind of up to you, however I would handle it in the controller (and throw it in the model). I usually try to call a different template if there is an error so as to avoid a bunch of conditionals, but sometimes you just have to put extra logic in your template instead.
Also, you have to ask yourself if this is really an exceptional condition - it might be easier to return null and handle that return value in your controller. I can't really tell from the data objects (value, service, and user) whether this is something that will happen all the time or not.