How do you handle errors when using a service layer? - php

in my Zend Framework project, I use a Service Layer, however I don't really know where to handle errors.
For example, let's say I've a UserService::updateUser($data);
What if I've:
$data = array(
'userId' => 2,
'firstName' => 'Jane',
'lastName' => 'Doe',
);
And user with id 2 doesn't exist?
Where and how would you handle such errors?

You can forward to a specific controller to handle all your business errors, like this :
if ($error=true)
return $this->_forward('standarderror', 'businesserror', 'default',
array('msgtitle' => $this->view->translate('item_not_found'),
'msg' => $this->view->translate('item_not_found_msg')));
and where your BusinesserrorController looks like :
class BusinesserrorController extends Zend_Controller_Action {
public function init() {
$this->_helper->viewRenderer->setNoRender();
}
public function standarderrorAction() {
$msgtitle = $this->_getParam('msgtitle');
$msg = $this->_getParam('msg');
$this->view->errortitle = $msgtitle;
$this->view->errormessage = $msg;
$this->view->nextstep = $this->view->translate('return_to_the_homepage');
$this->view->nextstepurl = "/";
echo $this->render('error/businesserror', null, true);
}
}
you can parametrize the forwarded url as well ;)

Related

laravel write proper test for sending email

I wonder how to write proper unit test for my email sending method. It's a problem because inside method I get data from Auth object. Should I send id of user in Request?
public function sendGroupInvite(Request $request){
foreach ($request->get('data') as $item){
$invitations = new \App\Models\Invitations();
$invitations->user_id = Auth::id();
$invitations->name = $item["name"];
$invitations->email = $item["email"];
$invitations->status = 0;
$invitations->token = \UUID::getToken(20);
$invitations->username = Auth::user()->name;
$invitations->save();
$settings = UserSettings::where('user_id', Auth::id())->first();
$email = $item["email"];
$url = 'https://example.com/invite/accept/'.$invitations->token;
$urlreject = 'https://example.com/invite/reject/'.$invitations->token;
$mailproperties = ['token' => $invitations->token,
'name' => $invitations->name,
'url' => $url,
'email' => $email,
'urlreject' => $urlreject,
'userid' => Auth::id(),
'username' => Auth::user()->name,
'user_name' => $settings->name,
'user_lastname' => $settings->lastname,
'user_link' => $settings->user_link,
];
$this->dispatch(new SendMail(new Invitations($mailproperties)));
}
return json_encode(array('msg' => 'ok'));
}
I'm using Auth to get username and user id. When I testing it it's not works, because Auth it's null.
I would go with mocking the queue, something similar to this. Mock Documentation
class MailTester extends TestCase{
/**
* #test
*/
public function test_mail(){
Queue::fake();
// call your api or method
Queue::assertPushed(SendMail, function(SendMail $job) {
return $job->something = $yourProperties;
});
}
You could try "acting as" to deal with the Auth::user().
...
class MyControllerTest extends TestCase{
/**
* #test
*/
public function foo(){
$user = App\Users::find(env('TEST_USER_ID')); //from phpunit.xml
$route = route('foo-route');
$post = ['foo' => 'bar'];
$this->actingAs($user)//a second param is opitonal here for api
->post($route, $post)
->assertStatus(200);
}
}

Can not access method in CakePHP

I am working in CakePHP 2.6.1 and I have a project in which I have to create an API. So I have created a function and its working fine when I am logged in but when I try to access without login, it redirects to the login page.
My function looks like :
class AndroidController extends AppController {
public function admin_survey_question()
{
$this->loadModel('Question');
Configure::write('debug', '2');
$survey_id = $_REQUEST['survey_id'];
$this->layout = "";
//$condition = "Question.survey_id = '".$survey_id."'";
$this->Question->unbindModel(
array('hasMany' => array('Result'))
);
$info = $this->Question->find('all', array(
'fields' => array('Question.id,Question.question, Question.options'),
'conditions' => array(
"Question.survey_id" => $survey_id /*dont use array() */
)
));
echo json_encode($info);
exit;
}
}
Here,In core.php there is a Routing.prefixes used as admin.
Configure::write('Routing.prefixes', array('admin','services'));
When I call this api
http://navyon.com/dev/mt/admin/android/survey_question?survey_id=2
then it redirects to the login page.
I need access api without login.So how can I resolve this problem?
To make accessible this method admin_survey_question without authentication, you need to allow it in beforeFilter
class AndroidController extends AppController {
public function beforeFilter() {
parent::beforeFilter();
$this->Auth->allow('admin_survey_question');
}
public function admin_survey_question()
{
$this->loadModel('Question');
Configure::write('debug', '2');
$survey_id = $_REQUEST['survey_id'];
$this->layout = "";
//$condition = "Question.survey_id = '".$survey_id."'";
$this->Question->unbindModel(
array('hasMany' => array('Result'))
);
$info = $this->Question->find('all', array(
'fields' => array('Question.id,Question.question, Question.options'),
'conditions' => array(
"Question.survey_id" => $survey_id /*dont use array() */
)
));
echo json_encode($info);
exit;
}
}
See Docs

restFUL API dependency on store

my app is a Book manager where I can create Books and Pages.
I have my bookController with a "store" on POST, which store a title and a description.
public function store()
{
$rules = array(
'title' => 'required|min:3',
'description' => 'required|min:30'
);
$validator = Validator::make(Input::all(), $rules);
if ($validator->fails()) {
return Response::json(
array(
'metadata' => array(
'error' => true,
'message' => 'The book creation has failed'
)
),
400
);
}
else {
$slug = Str::slug(Request::get('title'));
$existSlug = Book::where('slug',$slug)->get();
if(count($existSlug) > 0) {
return Response::json(
array(
'metadata' => array(
'error' => true,
'message' => 'This title is already taken'
)
),
400
);
}
else {
$book = new Book;
$book->title = Request::get('title');
$book->slug = $slug;
$book->description = Request::get('description');
$book->user_id = Auth::user()->id;
$book->status = false;
$book->save();
$stored = $book->toArray();
$metadata = array(
'metadata' => array(
'error' => false,
)
);
return Response::json(
array_merge($stored,$metadata),
201
);
}
}
}
I also have a pageController with a "store" on POST, which store a page content :
public function store()
{
$rules = array(
'content' => 'required|between:300,350',
'book_id' => 'required|exists:books,id'
);
$validator = Validator::make(Input::all(), $rules);
if($validator->fails()) {
return Response::json(
array(
'metadata' => array(
'error' => true,
'message' => 'The page must be between 300 and 350 characters'
)
),
400
);
}
else {
$book = Book::find(Input::get('book_id'));
$content = Input::get('content');
$parent = Page::where('book_id',$book->id)->where('status',1)->orderBy('id', 'desc')->first();
if($parent){
$parent_id = $parent->id;
$parent_number = $parent->number;
$status = 0; //Define the status of the created page
}
else{
//If it's the first page of the book
$parent_id = 0;
$parent_number = 0;
$status = 1; //if there's no parent page, the new page is the first - auto validated - page of the book.
if($book->user_id != Auth::user()->id) {
return Response::json(
array(
'metadata' => array(
'error' => true,
'message' => 'You have to be the author of a book to write the first page.'
)
),
403
);
}
}
$page = new Page;
$page->content = $content;
$page->book_id = $book->id;
$page->parent_id = $parent_id;
$page->number = $parent_number + 1;
$page->user_id = Auth::user()->id;
$page->status = $status;
$page->save();
$stored = $page->toArray();
$metadata = array(
'metadata' => array(
'error' => false
)
);
return Response::json(
array_merge($stored,$metadata),
201
);
}
}
Whenever someone creates a book, he has to write at least its first page. This result in a form with an input title, description and content.
I send a POST to [...]/books with my input title and description
If Success => I get the book id, and send it with the input content to [...]/pages.
Here are my problems :
Someone can send a post on [...]/books and will store a new book with no page
I want to solve this in the more "restFUL way", meaning no "hackish solution" like sending the content to /books and make a page validation in the bookController
Also, even if I chose the hackish way, my API is still not safe : I can stop the second request (to /pages) to be sent.
How do I handle this co-dependency ?
1st
Your controllers are doing too much, they are not supposed to know anything about your business logic this is something that should be handle by specific classes (models, repositories, domain logic classes).
Create some classes to handle this logic, send the Input to them and make it happen. Call them whatever you need to, using Laravel is great because you can do whatever you want with your code.
2nd
If you have different data constraints to be enforced, you can:
Handle them on the same request
Depends on your interface, if you have everything you need on a single page, you just send the data and handle it on a repository, which has access to all your models.
An example that can be used for both could be:
A book repository using Dependency Injection, which means that Book and Page will be automatically instantiated by Laravel:
class BookRepository {
__construct(Book $book, Page $page)
{
$this->book = $book;
$this->page = $page;
}
public function store($input)
{
if ( ! $this->book->validate($input) || !$this->page->validate($input))
{
return 'error';
}
$book->create(input);
$page->create($input);
}
}
A Base Model with your validation:
class Book extends BaseModel {
public function validate($input)
{
/// validate here and return
}
}
Your models and rules for each:
class Book extends BaseModel {
$book_rules = array(
'title' => 'required|min:3',
'description' => 'required|min:30'
);
}
class Page extends BaseModel {
$page_rules = array(
'content' => 'required|between:300,350',
'book_id' => 'required|exists:books,id'
);
}
And then you create your view having book info and page info, and which will POST to BookController#store:
class BookController extends Controller {
public function __controller(BookRepository $book_repository)
{
$this->book_repository = $book_repository;
}
public function store()
{
if ( ! $this->book_repository->store($input))
{
return Redirect::back()
->withErrors(
$this->book_repository
->validation
->messages()
->all()
);
}
return Redirect::to('success');
}
}
Again we are using Dependency Injection. $book_repository will be instantiated automatically. So your Controller doesn't need to know what a Book or a Page do, it just need to get the request and pass to a repository that will take care of everything.
It's not all there, but it's a start.
Handle them on different requests
This is usual. User send a request, app check and store data. User send a second request, app check it all and send back errors, if needed.
Handle them in background
This is a smarter way to do it. Your app will receive all data, in one or more requests, store them, check them using a queue worker and send e-mails to the user telling him that there are some data to be filled. Books with no pages can be deleted after some time. You don't risk having bad data and your user will know what's missing as soon as you do too.

CakePHP change DATABASE_CONFIG variables, based on user input for custom datasource

I am looking for a way to access and change the DATABASE_CONFIG variables, based on user input. Using CakePHP I created a custom datasource, based on the one provided in the docs, to access an external API. The API returns a JSON string containing the 12 most recent objects. I need to be able to change the page number in the API request to get the next 12 results, as well as accept a free text query entered by the user.
app/Config/Database.php
class DATABASE_CONFIG {
public $behance = array(
'datasource' => 'BehanceDatasource',
'api_key' => '123456789',
'page' => '1',
'text_query' => 'foo'
);
}
app/Model/Datasource/BehanceDataSource.php
App::uses('HttpSocket', 'Network/Http');
class BehanceDatasource extends DataSource {
public $description = 'Beehance datasource';
public $config = array(
'api_key' => '',
'page' => '',
'text_query' => ''
);
public function __construct($config) {
parent::__construct($config);
$this->Http = new HttpSocket();
}
public function listSources($data = null) {
return null;
}
public function describe($model) {
return $this->_schema;
}
public function calculate(Model $model, $func, $params = array()) {
return 'COUNT';
}
public function read(Model $model, $queryData = array(), $recursive = null) {
if ($queryData['fields'] === 'COUNT') {
return array(array(array('count' => 1)));
}
$queryData['conditions']['api_key'] = $this->config['api_key'];
$queryData['conditions']['page'] = $this->config['page'];
$queryData['conditions']['page'] = $this->config['text_query'];
$json = $this->Http->get('http://www.behance.net/v2/projects', $queryData['conditions']);
$res = json_decode($json, true);
if (is_null($res)) {
$error = json_last_error();
throw new CakeException($error);
}
return array($model->alias => $res);
}
}
Is there anyway to access and change the $behance array, or is there another way to go about accessing an external API with cakePHP that I am totally missing?

Zend_OpenId_Extension_Sreg fails on me

Below is a working example of my OpenId implementation. I use hyves.nl as the OpenId provider but this also works with me.yahoo.com and probably other OpenId providers as well (but not Google).
So far so good. But now I want to fetch the nickname and/or fullname from my hyves profile. But when I set nickname and/or fullname to true in the $props array I can't login anymore at all.
What am I doing wrong here?
class TestController extends Zend_Controller_Action
{
private $_sreg = null;
public function init()
{
$props = array('nickname' => false,
'email' => false,
'fullname' => false,
'dob' => false,
'gender' => false,
'postcode' => false,
'country' => false,
'language' => false,
'timezone' => false);
$this->_sreg = new Zend_OpenId_Extension_Sreg($props);
}
public function loginAction()
{
$consumer = new Zend_OpenId_Consumer();
if (!$consumer->login('hyves.nl', 'http://localhost/trouwcom/public/test/verify', 'http://localhost/trouwcom', $this->_sreg))
{
echo 'Login failed';
}
$this->_helper->viewRenderer->setNoRender();
}
public function verifyAction()
{
$consumer = new Zend_OpenId_Consumer();
if ($consumer->verify($_GET, $id, $this->_sreg))
{
echo 'VALID ' . htmlspecialchars($id);
$data = $this->_sreg->getProperties();
print_r($data);
}
else
{
echo 'INVALID ' . htmlspecialchars($id);
}
$this->_helper->viewRenderer->setNoRender();
}
}
Could it be this bug? http://framework.zend.com/issues/browse/ZF-5266
Don't know if you found your answer already. But if you didn't or anyone else is reading this question like me and having the same problem I just found the answer.
The problem with openId provider "hyves.nl" is that they don't return the verification parameters by $_GET but by $_POST.
# This works
$consumer->verify($_POST, $id, $this->_sreg);

Categories