I had been slowly learning PHP OOP, I decided it was time to start inserting into my table, however it doesn't seem to be inserting. I compared my code with working versions and I can't see what the problem might be, I attempted a var_dump(), the query returned as I expected, I tested my database class by creating an new user, it was successfully created so I assume it isn't that, I tested the SQL query and it was able to insert, I'm at a loss for it might be now
form
<?php
session_start();
require ("classes/Review.php");
require ("classes/Database.php");
if ($_SERVER['REQUEST_METHOD'] == 'POST')
{
$reviewing = new ReviewValidation();
$review = $reviewTitle = "";
$post = filter_input_array(INPUT_POST, FILTER_SANITIZE_STRING);
$reviewTitle = $post['reviewTitle'];
$review = $post['review'];
$errors = array();
$fields = array(
'title' => array(
'validate' => 'title',
'message' => 'Title must be at least fife characters',
'value' => $reviewTitle,
),
'review' => array(
'validate' => 'review',
'message' => 'Your review must be at least three hundred characters',
'value' => $review,
)
);
foreach($fields as $key => $value)
{
$validation_result = $reviewing->{$value['validate']}($value['value']);
if(!$validation_result)
{
$errors[] = ['name' => $key, 'error' => $value['message']];
}
}
if(empty($errors))
{
try
{
$db = new Database;
$success = ["message" => "Review subbbmitted"];
$process = $db->prepare('INSERT INTO reviews (reviewTitle)
VALUES
(:reviewTitle');
$process->bindValue(':reviewTitle', $reviewTitle);
$process->execute();
}
catch(Exception $e)
{
$errors[] = ['response' => 'fail'];
}
}
}
header('Content-Type: application/json');
if (empty($errors))
{
echo json_encode($success);
}
else
{
echo json_encode(["errors" => $errors]);
}
class
<?php
class ReviewValidation
{
private
$db,
$review,
$reviewTitle;
private static
$reviewLength = 50,
$rewviewtitleLength = 5;
public static function title($reviewTitle)
{
return(strlen($reviewTitle) >= self::$rewviewtitleLength);
}
public static function review($review)
{
return(strlen($review) >= self::$reviewLength);
}
}
Looks like you may be missing the closing ) in the insert query:
$process = $db->prepare('INSERT INTO reviews (reviewTitle)
VALUES
(:reviewTitle)');
If you add in a closing parenthesis after :reviewTitle, before the single quote your syntax will be correct (shown above).
I also noticed that your calls to the static methods in the ReviewValidation class are using the object operator (->). To access static methods you need to utilize the scope resolution operator.
So your $validation_result line should look like:
$validation_result = ReviewValidation::{$value['validate']}($value['value']);
I think because of this, the validation may have been passing, which is why you where getting the no default value issue.
Related
I have a php script that performs validation I have a code block that sets up 4 fields that need the same validation. What I want to achieve is that instead in this code block, I can setup the key name, the display name and field type and below set codes which would then automatically validate the fields according to the rules set for each field type.
Here is the code block in question:
// key name => display name
$fields = [
'firstName' => 'First Name',
'lastName' => 'Last Name',
'companyName' => 'Company Name',
'companyAddress' => 'Company Address',
];
So in this block I want to setup the key name, display name and field type. Currently I just got the these four fields. Is there a way I can achieve what I desire?
Here is my full code:
function validate($formData)
{
// Initiate Array
$validationMSG = array(); // array to hold validation errors
// what to validate (basics, i.e. required fields)
// key name => display name
$fields = [
'firstName' => 'First Name',
'lastName' => 'Last Name',
'companyName' => 'Company Name',
'companyAddress' => 'Company Address',
];
//simple loop
foreach($fields as $name => $display){
if(empty($formData[$name])){
$validationMSG[$name] = "${display} is required.";
}
}
//and NOW wee can perform some specific tests:
$pname_exp = '/^[a-zA-Z0-9\_]{2,20}/';
if(isset($formData['firstName']) && !preg_match($pname_exp, $formData['firstName'])){
$validationMSG['firstName'] = 'First Name is not valid.';
}
if(isset($formData['lastName']) && !preg_match($pname_exp, $formData['lastName'])){
$validationMSG['lastName'] = 'Last Name is required.';
}
//removed company name and company address checks, because we are done with them in the loop.
// Validate state
if (!isset($formData['state'])) {
$validationMSG['state'] = 'State is required.';
}
// Validate city
if (!isset($formData['city'])) {
$validationMSG['city'] = 'City is required.';
}
// Validate Zipcode - If Field is Empty
if (!isset($formData['zipcode'])) {
$validationMSG['zipcode'] = 'Zipcode is required.';
}
// Validate emailAddress
if (!isset($formData['emailAddress'])) {
$validationMSG['emailAddress'] = 'Email Address is required.';
}
// Check if emailAddress is a valid email address
elseif (!filter_var($formData['emailAddress'], FILTER_VALIDATE_EMAIL)) {
$validationMSG['emailAddress'] = 'Email address is not valid.';
}
//Validate phoneNumber
if (!isset($formData['phoneNumber'])) {
$validationMSG['phoneNumber'] = 'Phone Number is required.';
}
//Validate phoneNumber
elseif (preg_match('/^[0-9-\s]+$/D', $formData['phoneNumber'])) {
$validationMSG['phoneNumber'] = 'Must be a valid phone number.';
}
// Validate message
if (!isset($formData['message'])) {
$validationMSG['message'] = 'Message is required.';
}
if (!empty($validationMSG)) {
return $validationMSG;
}
else {
$captcha = checkCaptcha($formData['g-recaptcha-response']);
if(!$captcha['isSuccess']){
$validationMSG['captcha'] = 'ReCaptcha is required.';
return $validationMSG;
}
//End of Validation Function
}
}
//testing
$input = ['firstName' => 'John'];
$errors = validate($input);
var_dump($errors);
You should be using an approach like this. This is a starting point to write a better validation check in your loops:
<?php
function validate($formData)
{
// Initiate Array
$validationMSG = array(); // array to hold validation errors
// what to validate (basics, i.e. required fields)
// key name => display name
$fields = [
'firstName' => [
'label' => 'First Name',
'rules' => 'required'
],
'lastName' => [
'label' => 'Last Name',
'rules' => 'required'
],
'emailAddress' => [
'label' => 'Email',
'rules' => 'required|email'
]
];
//simple loop
foreach($fields as $fieldName => $args) {
$rules = explode('|', $args['rules']);
foreach($rules as $rule)
{
if($rule == 'required' && (!isset($formData[$fieldName]) || empty($formData[$fieldName])))
{
$validationMSG[$fieldName][] = sprintf('%s is a required field.', $args['label']);
}
if((isset($formData[$fieldName]) && $rule == 'email') && !filter_var($formData[$fieldName], FILTER_VALIDATE_EMAIL))
{
$validationMSG[$fieldName][] = sprintf('%s must be a valid email.', $args['label']);
}
}
}
return $validationMSG;
}
This can be improved but the concept will get you started.
Here is a way to do validation, while still having a format that can be promoted to a typed class (e.g., with namespace and dependency injection, etc) on a refactor. What I will show you is a way to acquire an object in such a way you can validate the message payload, based on a common naming construct.
First, create a regular php function called userActions(). This will be used to (globally or as an include where needed) call into and get these objects. This will return a closure.
function userActions(): \Closure { ... }
Next, you'll need to encapsulate your procedure. I recommend doing it like this:
$startUserRecord = static function(
string $firstName,
string $lastName,
string $companyName,
string $companyAddress
) {
return new class(...func_get_args()) {
private $payload = [];
private $firstName;
private $lastName;
private $companyName;
private $companyAddress;
public function __construct(
string $firstName,
string $lastName,
string $companyName,
string $companyAddress
) {
$this->payload = func_get_args();
$this->firstName = $firstName;
$this->lastName = $lastName;
$this->companyName = $companyName;
$this->companyAddress = $companyAddress;
}
public function validate(): array {
// Here is where you would actually validate this one message.
// Empty array means no validation messages (errors).
return [];
}
public function payload(): array {
return $this->payload;
}
};
};
// Other ones go here, too.
$saveUserProfile = static function() { ... };
$resetUserLogin = static function() { ... };
Corral them up to together, so we can use() them in our userActivities() closure:
$activities= [
'startUserRecord' => $startUserRecord,
'saveUserProfile ' => $saveUserProfile,
'resetUserLogin ' => $resetUserLogin,
];
Now, I've called it startUserRecord because I needed to make up a command name; give it the name you wish, however I recommend verb + subject + context construct, generally.
Static checks work for the "string, integer" and other low-level type checks, or class-based constructs as you have available (say, an Email value object). The validation of the values, as you can see, occurs inside the validate() method.
Next, create the closure that you'll return that actually allows you to search for and get only the command objects you want:
return static function(/* autoset as $get */) use($activities) {
$get = func_get_args();
$find = static function($name) use(&$get) {
return in_array($name, $get);
};
$filtered = array_filter($activities, $find, ARRAY_FILTER_USE_KEY);
$found = [];
// Keep in the same order.
foreach($get as $name) {
$found[] = $filtered[$name];
}
return $found;
};
Pay attention to the use() statement on the closure(s).
This is put within the userActivities() function declaration. Now, you can call and set it up, and generally use what you have:
$fields = [
'firstName' => 'First Name',
'lastName' => 'Last Name',
'companyName' => 'Company Name',
'companyAddress' => 'Company Address',
];
$userActivities = userActions();
[$startUserRecord] = $userActivities('startUserRecord');
$activity = $startUserRecord(...array_values($fields));
if ($errors = $activity->validate()) {
throw new InvalidCommand($errors);
}
// Do something with $activity->payload().
Note:
I'm not modifying the payload. You can add a conditioner/sanitizer method that acts on the payload or return the object's properties in some form.
We're destructuring with the [$startUserRecord] line. This allows you to return multiple entries and load them into their own variable names. An example:
[$saveUserProfile,$resetUserLogin] = $userActivites('saveUserProfile','resetUserLogin');
$startUserRecord(...array_values($fields)) This is called a spread or splat operation (splat operator: ... in the invocation), and makes each array item it's own separate argument, in order. I'm getting to that with array_values() here, however it's better to pass in an actual array without the keys, to maintain order in all cases.
Here it is put together: https://3v4l.org/sn61Q
This is just a starting point. Change it up, do what you need to, this should give you some idea what you can accomplish.
For instance, you could move the initializers to a with() method and "autoload" little closure-based validators that you share within the userActivities() function, like a $validateEmail() or $validatePhone() closures you declare and then use($validateEmail, ...) and within that closure new class($validateEmail, ...) to share these within an activity's context.
I would like to read about your opinions on what the best way to create a validation class is.
Below i pasted in my version, it's not really extensive yet, but is it the right approach so far?
I want every element which could appear in a form and which should be validated to have its own properties, in terms of string length for strings or file size for images or files in general (see below).
Also, is it better to declare these rules as nested arrays or should I put them in one big string, which would then be split up during the process?
mb_internal_encoding("UTF-8");
class Phrase {
//creates parts of sentences, not important
static function additive(array $limbs) {
return implode(' and ', array_filter([implode(', ', array_slice($limbs, 0, -1)), end($limbs)], 'strlen'));
}
}
class Text {
static function validate($item) {
$err = array();
$value = $_POST[$item] ?? $_GET[$item];
$criteria = FormProcess::$criteria[$item];
foreach($criteria as $critKey => $critVal) {
if($critKey === 'required' && empty($value)) {
$err[] = "is required";
} else if(!empty($value)) {
switch($critKey) {
case 'length':
if(is_array($critVal)) {
//min and max set
if(mb_strlen($value) < $critVal[0] || mb_strlen($value) > $critVal[1]) {
$this->err[] = "must contain between {$critVal[0]} and {$critVal[1]} characters";
}
} else {
//max set only
if(mb_strlen($value) > $critVal) {
$err[] = "must contain a maximum of $critVal characters";
}
}
break;
case 'pattern':
if(!preg_match($critVal[0], $value)) {
$err[] = "may consist of {$critVal[1]} only";
}
break;
case 'function':
$result = static::$critVal($value);
if($result) {
$err[] = $result;
}
break;
}
}
}
if(!empty($err)) {
return "{$criteria['name']} " . Phrase::additive($err) . "!";
}
return false;
}
private static function email($email) {
//checks if given string is a valid email address
if(!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return "is invalid";
}
return false;
}
}
class File {
//checks for aspects like filesize...
static function validate() {
//...
}
}
class Image extends File {
static function validate() {
parent::validate(); //perform general file checks first
//...checks for images specifically
}
}
class Video extends File {
static function validate() {
parent::validate(); //perform general file checks first
//...checks for videos specifically
}
}
class FormProcess {
public $errors;
//declare, what kind of requirements the items must meet
static $criteria = array(
'email' => array(
'type' => 'Text',
'required' => true,
'name' => 'Email',
'length' => 48,
'function' => 'email',
),
'username' => array(
'type' => 'Text',
'required' => true,
'name' => 'Username',
'length' => [4, 24],
'pattern' => ['/^[-\w]+$/', "alphanumeric characters, underscores and hyphens"],
),
'password' => array(
'type' => 'Text',
'required' => true,
'name' => 'Password',
'length' => [6, 100],
'pattern' => ['/^[\S]+$/', "non-whitespace characters"],
),
);
//runs the validate function on each item while storing occuring errors
function __construct(array $items) {
foreach($items as $item) {
$class = self::$criteria[$item]['type'];
$result = $class::validate($item);
if($result) {
$this->errors[] = $result;
}
}
}
}
Then all you had to do, is, naming all expected items (by their html 'name' attribute in the form) in an array and pass it through the constructor, which would then run the appropriate validation function on each item.
$expected = ['username', 'password'];
$signup = new FormProcess($expected);
if($signup->errors) {
?>
There was something wrong with your request:
<ul>
<?php foreach($signup->errors as $error) { ?>
<li><?= $error ?></li>
<?php } ?>
</ul>
<?php
}
I hope to learn from mistakes and make improvements to the code from you where they are needed.
Thank you in advance!
I am writing the code with the following for mat in rest api.
I thought that, validation done in controller and the service layer cares of writing business logic and model takes care of database operations. I hope I am correct.
My clarification here is whether I can send var_id (underscore separated) to the service layer or as varID (camel-case).
I searched that lot of the api calls, most of them are var_id, that's the reason I used myself too.
But how can I use the variable here, because zend framework code works with camel-case, if am assigning the variables varID = var_id for each and every variable, is it right.
$dataSendToService = array(
$varID = var_id,
$varID2 = var_id2;
);
I am calling the api like the below in the create method.
http://128.12.788.88/api/v1/users/72
json get method like this
{
"var_id":"var_value",
"var_id1":"var_value1"
}
In controller:
function create() {
$body = $this->getRequest()->getContent();
$data = json_decode($body);
$id = $this->params('id');
//validation
if( !isset( $data->pat_id ) || empty( $data->pat_id ) ) {
$resp = array(
'status' => 'failure',
'errorCode' => 531,
'errorMessage' => 'Patient ID should not be empty'
);
return new JsonModel($resp);
}
if( !isset( $data->doc_id ) || empty($data->doc_id )) {
$resp = array(
'status' => 'failure',
'errorCode' => 532,
'errorMessage' => 'Doctor ID should not be empty'
);
return new JsonModel($resp);
}
if( !isset( $data->apt_time ) || empty($data->apt_time )) {
$resp = array(
'status' => 'failure',
'errorCode' => 533,
'errorMessage' => 'Appointment time should not be empty');
return new JsonModel($resp);
}
if( !isset( $data->apt_subject ) || empty($data->apt_subject )) {
$resp = array(
'status' => 'failure',
'errorCode' => 534,
'errorMessage' => 'Appointment Subject time should not be empty');
return new JsonModel($resp);
}
$sm = $this->getServiceLocator();
$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
$usersService = new UsersService($dbAdapter);
$resp = $usersService->profile($data,$id);
}
In service:
function create() {
//get the data and pass it to model
}
In model:
function create() {
//get the data and insert in table and return the result
}
It is totally fine to use underscore separated values in ZF2 but indeed camel-casing seems to be more common practice.
You definitely don't have to do all this manually, you can easily use filters for changing your json variables to camel-case:
use Zend\Filter\Word\CamelCaseToUnderscore;
...
$filter = new CamelCaseToUnderscore();
print $filter->filter('ThisIsMyContent');
And back to underscore separated:
use Zend\Filter\Word\CamelCaseToDash;
...
filter = new CamelCaseToDash();
print $filter->filter('ThisIsMyContent');
If you use a hydrator then you can use the ZF2 ClassMethods hydrator which can be set to extract and hydrate between both by passing a boolean to the constructor:
underscore-separated (true) or camel-case (false)
use Zend\Stdlib\Hydrator\ClassMethods;
...
$boolean = true|false;
$hydrator = new ClassMethods($boolean)
I'm currently working on a project using the Phalcon Framework that has pages with complex forms and a lot of inputs, to break it down nicely I'm dividing the forms into a step-by-step process.
How would one validate the form on each step before going to the next step and then save the whole form on the final step?
I can't seem to find anything documented about this sort of process as it likes to validate the form in it's entirety if I use the form builder.
Simple, just create a custom methods in your form class to validate any step, and the posted data from some step save into message class and store it into session by "stepX", when posted data is not valid just set defaults from post. When valid save it into session as i describe above.
For example how i mean "controller"
<?php
class MyController extends BaseController {
public function processStep1Action(){
$form = new MyForm();
if($this->request->isPost()){//im using my custom request class
if(!$form->isValid($this->request->getPost()){
//error messages goes here
$form->setDefaultsFromRequest($this->request); // it will set the filled data
}
else {
$messageClass = new MyMessageContainer();
$messageClass->setData($this->request);//inside parse requested data into message class, or parse it as $messageClass->name = $this->request->getPost('name');
$this->session->save('step1',$messageClass); //maybe it would be want to serialize it
//then redirect to the step 2 or x
}
}
}
}
So in the next step you can access data from sessions $this->session->get('step1'); so you can in final step load all posted data and store it into DB.
I hope this helps! :)
here is my form maybe it can be helpful for you.
<?php
namespace Manager\Library\Forms\User;
use Phalcon\Forms\Form,
Phalcon\Forms\Element\Email,
Phalcon\Forms\Element\Select,
Phalcon\Forms\Element\Password,
Phalcon\Forms\Element\Check,
Phalcon\Validation\Validator\Confirmation,
Phalcon\Validation\Validator\StringLength,
Phalcon\Forms\Element\Submit,
Phalcon\Validation\Validator\PresenceOf,
Model\Group;
class AddUser extends Form {
public function initialize()
{
$email = new Email('email');
$email->addValidators(array(
new \Phalcon\Validation\Validator\Email(array(
'message' => 'Nezadali jste email nebo má nesprávny tvar(email#domena.tld).'
))
));
$this->add($email);
$this->initGroupElement();
$password = new Password('password');
$password
->addValidator(new StringLength(array('min' => 6,'messageMinimum' => 'Nezadali jste heslo nebo je příliš krátke, minimální počet znaků je 6.')))
->addValidator(new Confirmation(array('with' => 'password-again',"message" => "Zadané hesla se neshodují.")));
$this->add($password);
$repeatPassword = new Password('password-again');
$this->add($repeatPassword);
$this->initializeProfileElements();
$active = new Check('active',array('value' => 1));
$this->add($active);
$this->add( new Submit('save') );
\Phalcon\Tag::setDefault('password', '');
\Phalcon\Tag::setDefault('password-again', '');
}
public function initializeEdit(){
$email = new Email('email');
$email->addValidators(array(
new \Phalcon\Validation\Validator\Email(array(
'message' => 'Nezadali jste email nebo má nesprávny tvar(email#domena.tld).'
))
));
$this->add($email);
$this->initGroupElement();
$password = new Password('password');
$this->add($password);
$repeatPassword = new Password('password-again');
$this->add($repeatPassword);
$this->initializeProfileElements();
$active = new Check('active',array('value' => 1));
$this->add($active);
$this->add( new Submit('save') );
\Phalcon\Tag::setDefault('password', '');
\Phalcon\Tag::setDefault('password-again', '');
}
protected function initGroupElement(){
$auth = \Core\Auth::getIdentity();
$groups = new Group();
// $groups->addColumns(array('id','name'));
//set global condition about Super Admin
$groups->addFilter('id', 1,'<>');
if($auth){
//set restrictions for main groups
if((int)$auth->group_id === 1){ //super admingroup
//no filter
}
else if((int)$auth->group_id === 2){ //admin group
$groups->addFilter('id', 1,'>');
}
else if((int)$auth->group_id === 6){//Provozovatel group
$groups->addFilter('id',array(3,6,7));
$groups->addFilter('public', 1,'=',true);
}
else { // other groups
$groups->addFilter('public', 1);
}
}
$groups = $groups->findFiltered();
$groupElement = new Select('group');
foreach($groups as $group){
$groupElement->addOption(array($group->id => $group->name));
}
$this->add($groupElement);
}
protected function initializeProfileElements(){
$forename = new \Phalcon\Forms\Element\Text('forename');
$this->add($forename);
$surname = new \Phalcon\Forms\Element\Text('surname');
$this->add($surname);
$street = new \Phalcon\Forms\Element\Text('street');
$this->add($street);
$postal = new \Phalcon\Forms\Element\Text('postal');
$this->add($postal);
$city = new \Phalcon\Forms\Element\Text('city');
$this->add($city);
$ic = new \Phalcon\Forms\Element\Text('ic');
$this->add($ic);
$dic = new \Phalcon\Forms\Element\Text('dic');
$this->add($dic);
}
public function setDefault($fieldName,$value){
\Phalcon\Tag::setDefault($fieldName, $value);
}
public function setDefaults($object){
if($object instanceof \Model\User){
$this->setDefaultsFromObject($object);
}
else if($object instanceof \Phalcon\Http\Request){
$this->setDefaultsFromRequest($object);
}
}
protected function setDefaultsFromObject(\Model\User $user){
$profile = $user->getRelated('\Model\Profile');
\Phalcon\Tag::setDefaults(array(
'email' => $user->email,
'group' => $user->group_id,
'active' => $user->active,
'forename' => $profile->forename,
'surname' => $profile->surname,
'street' => $profile->street,
'city' => $profile->city,
'postal' => $profile->postal,
'ic' => $profile->IC,
'dic' => $profile->DIC
));
}
protected function setDefaultsFromRequest(\Phalcon\Http\Request $request){
\Phalcon\Tag::setDefaults(array(
'email' => $request->getPost('email'),
'group' => $request->getPost('group'),
'active' => $request->getPost('active')
));
\Phalcon\Tag::setDefaults(array(
'forename' => $request->getPost('forename'),
'surname' => $request->getPost('surname'),
'street' => $request->getPost('street'),
'city' => $request->getPost('city'),
'postal' => $request->getPost('postal'),
'ic' => $request->getPost('ic'),
'dic' => $request->getPost('dic')
));
}
}
In addition to Kamil's answer, another option to consider is to use Javascript on the front-end to handle your multi-step form. This will add some complexity as you will need to have the javascript to handle the form steps and do preliminary validation, but it only requires a single submit where you can validate content within a single method.
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.