Sign up function on Zend 3

Today we are going to implement a sign up form within our authentication module we’ve set up before.

Note that we don’t want to be in competition with Zf Commons with their registration and login module, this article is more like a tutorial to train you on using Zend and handling a form.

In later tutorials we will teach you how to implement different things starting from this module, for instance SMS validation, API calls to third part applications and so on…

I assume you have already followed the user demo page on Github and the signin module we’ve integrated before, if not then it’s time to do it, click on the link shown up here.

Adding a Sign up form

Let’s stay simple and require only an email and a password but for the fun we will use a 2 steps registration form, check it out :

Email and Password fields On submission, the email and password are stored in the session
Firstname and Lastname fields + Company name & Company type & Company Identification number Upon submission, an email is dispatched to the user to confirm the registration process. All data are being stored.

Where shall we start ?

To make things clear, let’s add a table into our base to store additional information about the end user.

Database

Doctrine migrations will be the choice again for this tutorial, in a later tutorial we will see what Yaml has to offer and if it does save us time in the overall process.

Remember the tables we used for the authentication process ?

This is taken from the Mysql workbench tool.

We need 2 more tables for our registration process, create first a new version of your migration :

$./vendor/bin/doctrine-module migrations:generate

Then add these lines to the new generated file :


/**
* @param Schema $schema
*/
public function up(Schema $schema)
{

// Create 'legal' table - Hold legal status of a company
$table = $schema->createTable('legal');
$table->addColumn('id', 'smallint', ['autoincrement'=>true]);
$table->addColumn('status_name', "string", array("length" => 32,'notnull' => true));
$table->setPrimaryKey(['id']);
$table->addUniqueIndex(['status_name'],'status_name_index',[]);
$table->addOption('engine' , 'InnoDB');

// Create 'company' table
$table = $schema->createTable('company');
$table->addColumn('id', 'integer', ['autoincrement'=>true]);
$table->addColumn('company_identification', "string", array("length" => 32,'notnull' => true));
$table->addColumn('company_name', "string", array("length" => 32,'notnull' => true));
$table->addColumn('vat_number', "string", array("length" => 256 , 'notnull' => false));
$table->addColumn('legal_status_id', "smallint", array('notnull' => true));
$table->addColumn('date_created', 'datetime', ['notnull'=>true]);
$table->setPrimaryKey(['id']);
$table->addUniqueIndex(['company_name'],'company_name_index',[]);
$table->addForeignKeyConstraint('legal', ['legal_status_id'], ['id'], [], 'legal_id_fk');
$table->addOption('engine' , 'InnoDB');

// Create 'user' table
$table = $schema->createTable('user');
$table->addColumn('id', 'integer', ['autoincrement'=>true]);
$table->addColumn('company_id', "integer", array('notnull' => true));
$table->addColumn('user_email', "string", array("length" => 32,'notnull' => true));
$table->addColumn('user_password', "string", array("length" => 256 , 'notnull' => true));
$table->addColumn('user_name', "string", array("length" => 32 , 'notnull' => true));
$table->addColumn('pwd_reset_token', "string", array("length" => 32 , 'notnull' => false));
$table->addColumn('pwd_reset_token_creation_date', 'datetime', ['notnull'=>false]);
$table->addColumn('date_created', 'datetime', ['notnull'=>true]);
$table->setPrimaryKey(['id']);
$table->addUniqueIndex(['user_email','company_id'],'email_company_index',[]);
$table->addForeignKeyConstraint('company', ['company_id'], ['id'], [], 'company_id_fk');
$table->addOption('engine' , 'InnoDB');

// Create 'log' table
$table = $schema->createTable('log');
$table->addColumn('id', 'integer',['autoincrement'=>true]);
$table->addColumn('user_id', 'integer', ['notnull'=>true]);
$table->addColumn('date_log', 'datetime', ['notnull'=>true]);
$table->setPrimaryKey(['id']);
$table->addUniqueIndex(['user_id','date_log'],'user_date_index',[]);
$table->addForeignKeyConstraint('user', ['user_id'], ['id'], [], 'user_id_fk');
$table->addOption('engine' , 'InnoDB');
}

/**
* @param Schema $schema
*/
public function down(Schema $schema)
{
// this down() migration is auto-generated, please modify it to your needs
$schema->dropTable('log');
$schema->dropTable('user');
$schema->dropTable('company');
$schema->dropTable('legal');

}

Once you made the changes, issue the command :

$ ./vendor/bin/doctrine-module migrations:migrate

Checkout the Mysql result diagram :

Registration module Database

You can appreciate the relationships between the tables and to sum up things, check the list here :

  • One company can share multiple users – this is a on to many relation
  • One user can have multiple logs – one to many
  • Several companies can have the same legal status – many to one

Entities

How do we handle the data within the tables ? Each table represents an entity, we already have 2 classes : User & Log.

Thus we need 2 more : Company & Legal.

Let’s begin with the legal class :

<?php
namespace MyAuth\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * This class represents a legal status.
 * @ORM\Entity
 * @ORM\Table(name="legal")
 */
class Legal
{
    
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(name="id")
     */
    protected $id;
    
    /**
     * @ORM\Column(name="status_name")
     */
    protected $statusName;
    
    
    // Returns ID of this status.
    public function getId()
    {
        return $this->id;
    }
    
    // Sets ID of this status.
    public function setId($id)
    {
        $this->id = $id;
    }
    // Returns name of this status.
    public function getStatusName()
    {
        return $this->statusName;
    }
    
    // Sets name of this status.
    public function setStatusName($name)
    {
        $this->statusName = $name;
    }
    

}

Remember to respect the case.

The Company class :

<?php
namespace MyAuth\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;

/**
 * This class represents a single company
 * @ORM\Entity
 * @ORM\Table(name="company")
 */
class Company
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(name="id")
     */
    protected $id;
    
    /**
     * @ORM\Column(name="company_identification")
     */
    protected $companyIdentification;
    
    /**
     * @ORM\Column(name="company_name")
     */
    protected $companyName;
    
    /**
     * @ORM\Column(name="date_created")
     */
    protected $dateCreated;
    
    /**
     * @ORM\Column(name="vat_number")
     */
    protected $vatNumber;
    
    /**
     * @ORM\ManyToOne(targetEntity="\MyAuth\Entity\Legal")
     * @ORM\JoinColumn(name="legal_status_id", referencedColumnName="id") 
     */
    protected $status;
    

    /**
     * Constructor.
     */
    public function __construct()
    {
        $this->status = new ArrayCollection();
    }
    
    /**
     * Returns status for this company.
     * @return array
     */
    public function getStatus()
    {
        return $this->status;
    }
    
    /**
     * Adds a new status to this company.
     * @param $status
     */
    public function setStatus($id)
    {
        $this->status = $id;
    }
    
    // Returns ID of this company.
    public function getId()
    {
        return $this->id;
    }
    
    
    // Returns company name.
    public function getCompanyName()
    {
        return $this->companyName;
    }
    
    // Sets Company name
    public function setCompanyName($name)
    {
        $this->companyName= $name;
    }
    
    
    // Returns compnay identification
    public function getCompanyIdentification()
    {
        return $this->companyIdentification;
    }
    
    // Sets company identification
    public function setCompanyIdentification($identification)
    {
        $this->companyIdentification= $identification;
    }
    
    // Returns the date when this user was created.
    public function getDateCreated()
    {
        return $this->dateCreated;
    }
    
    // Sets the date when this user was created.
    public function setDateCreated($dateCreated)
    {
        $this->dateCreated = $dateCreated;
    }
    
}

We also need to add a few lines of code inside our User class :


/* .. */

/**
* @ORM\Column(name="company_id"
* @ORM\ManyToOne(targetEntity="\MyAuth\Entity\Company")
* @ORM\JoinColumn(name="company", referencedColumnName="id")*
*/
protected $company;

/*

...

*/
// Returns ID of company .
public function getCompany()
{
return $this->company;
}

// Sets ID of this user's company.
public function setCompany($id)
{
$this->company= $id;
}

How about dealing with our registration form and view container ?

The Registration form

As this is straightforward, just create a new file under the Form directory as follow :

<?php
namespace MyAuth\Form;

use Zend\Form\Form;
use Zend\InputFilter\InputFilter;
use Application\Validator\PhoneValidator;
//use MyAuth\Validator\LegalExistsValidator;
/**
 * This form is used to collect user registration data - This is a multi step form.
 */
class RegistrationForm extends Form
{
    
    /**
     * Entity manager.
     * @var Doctrine\ORM\EntityManager
     */
    private $entityManager = null;
    
    /**
     * Constructor.
     */
    public function __construct($step, $entityManager = null)
    {
        // Check input.
        if (!is_int($step) || $step < 1 || $step > 2)
        {
            throw new \Exception('Step is invalid');
        }    
            // Define form name
            parent::__construct('registration-form');
            
            // Set POST method for this form
            $this->setAttribute('method', 'post');
            
            $this->entityManager = $entityManager;
            
            $this->addElements($step);
            $this->addInputFilter($step); 
    }
    
    /**
     * This method adds elements to form (input fields and submit button).
     */
    protected function addElements($step)
    {
        
        if ($step==1) 
        {
            // Add "email" field
            $this->add([
                    'type'  => 'text',
                    'name' => 'email',
                    'options' => [
                            'label' => 'Your E-mail',
                    ],
            ]);
            
            // Add "full_name" field
            $this->add([
                    'type'  => 'text',
                    'name' => 'full_name',
                    'attributes' => [
                            'id' => 'full_name'
                    ],
                    'options' => [
                            'label' => 'Full Name',
                    ],
            ]);
            
            // Add "password" field
            $this->add([
                    'type'  => 'password',
                    'name' => 'password',
                    'options' => [
                            'label' => 'Password',
                    ],
            ]);
            
            // Add "confirm password" field
            $this->add([
                    'type'  => 'password',
                    'name' => 'confirm_password',
                    'options' => [
                            'label' => 'Confirm password',
                    ],
            ]);
            
    
        } elseif ($step == 2)
        {
            // Add "Company name" field
            $this->add([
                    'type'  => 'text',
                    'name' => 'company_name',
                    'attributes' => [
                            'id' => 'company_name'
                    ],
                    'options' => [
                            'label' => 'Company Name',
                    ],
            ]);
            
            // Add "Company ID" field
            $this->add([
                    'type'  => 'text',
                    'name' => 'company_identification',
                    'attributes' => [
                            'id' => 'company_identitification'
                    ],
                    'options' => [
                            'label' => 'Company ID',
                    ],
            ]);
            
            
            // Add "phone" field
            $this->add([
                    'type'  => 'text',
                    'name' => 'phone',
                    'attributes' => [
                            'id' => 'phone'
                    ],
                    'options' => [
                            'label' => 'Mobile Phone',
                    ],
            ]);
            
            // Add "street_address" field
            $this->add([
                    'type'  => 'text',
                    'name' => 'street_address',
                    'attributes' => [
                            'id' => 'street_address'
                    ],
                    'options' => [
                            'label' => 'Street address',
                    ],
            ]);
            
            // Add "city" field
            $this->add([
                    'type'  => 'text',
                    'name' => 'city',
                    'attributes' => [
                            'id' => 'city'
                    ],
                    'options' => [
                            'label' => 'City',
                    ],
            ]);
            
            // Add "state" field
            $this->add([
                    'type'  => 'text',
                    'name' => 'state',
                    'attributes' => [
                            'id' => 'state'
                    ],
                    'options' => [
                            'label' => 'State',
                    ],
            ]);
            
            // Add "post_code" field
            $this->add([
                    'type'  => 'text',
                    'name' => 'post_code',
                    'attributes' => [
                            'id' => 'post_code'
                    ],
                    'options' => [
                            'label' => 'Post Code',
                    ],
            ]);
            
            // Add "country" field
            $this->add([
                    'type'  => 'select',
                    'name' => 'country',
                    'attributes' => [
                            'id' => 'country',
                    ],
                    'options' => [
                            'label' => 'Country',
                            'empty_option' => '-- Please select --',
                            'value_options' => [
                                    'US' => 'United States',
                                    'CA' => 'Canada',
                                    'BR' => 'Brazil',
                                    'GB' => 'Great Britain',
                                    'FR' => 'France',
                                    'IT' => 'Italy',
                                    'DE' => 'Germany',
                                    'RU' => 'Russia',
                                    'IN' => 'India',
                                    'CN' => 'China',
                                    'AU' => 'Australia',
                                    'JP' => 'Japan'
                            ],
                    ],
            ]);
            
            // Add "status" field
            $this->add([
                    'type'  => 'select',
                    'name' => 'status',
                    'attributes' => [
                            'id' => 'status',
                    ],
                    'options' => [
                            'label' => 'Legal Status',
                            'empty_option' => '-- Please select --',
                            'value_options' => $this->entityManager->getLegalStatus(),
                    ],
            ]);
            
        } 
        
        // Add the CSRF field
        $this->add([
                'type'  => 'csrf',
                'name' => 'csrf',
                'attributes' => [],
                'options' => [
                        'csrf_options' => [
                                'timeout' => 600
                        ]
                ],
        ]);
        
        // Add the submit button
        $this->add([
                'type'  => 'submit',
                'name' => 'submit',
                'attributes' => [
                        'value' => 'Next Step',
                        'id' => 'submitbutton',
                ],
        ]);
    
    }
    
    /**
     * This method creates input filter (used for form filtering/validation).
     */
    private function addInputFilter($step)
    {
        // Create main input filter
        $inputFilter = new InputFilter();
        $this->setInputFilter($inputFilter);
        
        
        if ($step==1) 
        {
            // Add input for "email" field
            $inputFilter->add([
                    'name'     => 'email',
                    'required' => true,
                    'filters'  => [
                            ['name' => 'StringTrim'],
                    ],
                    'validators' => [
                            [
                                    'name' => 'EmailAddress',
                                    'options' => [
                                            'allow' => \Zend\Validator\Hostname::ALLOW_DNS,
                                            'useMxCheck' => false,
                                    ],
                            ],
                    ],
            ]);
            
            // Add input for "password" field
            $inputFilter->add([
                    'name'     => 'password',
                    'required' => true,
                    'filters'  => [
                    ],
                    'validators' => [
                            [
                                    'name'    => 'StringLength',
                                    'options' => [
                                            'min' => 6,
                                            'max' => 64
                                    ],
                            ],
                    ],
            ]);
            
            $inputFilter->add([
                    'name'     => 'full_name',
                    'required' => true,
                    'filters'  => [
                            ['name' => 'StringTrim'],
                            ['name' => 'StripTags'],
                            ['name' => 'StripNewlines'],
                    ],
                    'validators' => [
                            [
                                    'name'    => 'StringLength',
                                    'options' => [
                                            'min' => 1,
                                            'max' => 128
                                    ],
                            ],
                    ],
            ]);
            
            // Add input for "confirm_password" field
            $inputFilter->add([
                    'name'     => 'confirm_password',
                    'required' => true,
                    'filters'  => [
                    ],
                    'validators' => [
                            [
                                    'name'    => 'Identical',
                                    'options' => [
                                            'token' => 'password',
                                    ],
                            ],
                    ],
            ]);
            
        } elseif ($step == 2) 
        {
            $inputFilter->add([
                    'name'     => 'phone',
                    'required' => true,
                    'filters'  => [
                    ],
                    'validators' => [
                            [
                                    'name'    => 'StringLength',
                                    'options' => [
                                            'min' => 3,
                                            'max' => 32
                                    ],
                            ],
                            [
                                    'name' => PhoneValidator::class,
                                    'options' => [
                                            'format' => PhoneValidator::PHONE_FORMAT_INTL
                                    ]
                            ],
                    ],
            ]);
            
            // Add input for "company_name" field
            $inputFilter->add([
                    'name'     => 'company_name',
                    'required' => true,
                    'filters'  => [
                            ['name' => 'StringTrim'],
                    ],
                    'validators' => [
                            ['name'=>'StringLength', 'options'=>['min'=>1, 'max'=>255]]
                    ],
            ]);
            
            // Add input for "company_identification" field
            $inputFilter->add([
                    'name'     => 'company_identification',
                    'required' => true,
                    'filters'  => [
                            ['name' => 'StringTrim'],
                    ],
                    'validators' => [
                            ['name'=>'StringLength', 'options'=>['min'=>1, 'max'=>255]]
                    ],
            ]);
            
            // Add input for "status" field
            $inputFilter->add([
                    'name'     => 'status',
                    'required' => true,
                    'filters'  => [['name' => 'Int']
                    ],
                    'validators' => [
                            ['name' => 'IsInt'],
                            ['name'=>'Between', 'options'=>['min'=>0, 'max'=>999999]],
                            /*[
                                    'name' => LegalExistsValidator::class,
                                    'options' => [
                                            'entityManager' => $this->entityManager                                            
                                    ],
                            ],*/
                    ],
            ]);
            
            // Add input for "street_address" field
            $inputFilter->add([
                    'name'     => 'street_address',
                    'required' => true,
                    'filters'  => [
                            ['name' => 'StringTrim'],
                    ],
                    'validators' => [
                            ['name'=>'StringLength', 'options'=>['min'=>1, 'max'=>255]]
                    ],
            ]);
            
            // Add input for "city" field
            $inputFilter->add([
                    'name'     => 'city',
                    'required' => true,
                    'filters'  => [
                            ['name' => 'StringTrim'],
                    ],
                    'validators' => [
                            ['name'=>'StringLength', 'options'=>['min'=>1, 'max'=>255]]
                    ],
            ]);
            
            // Add input for "state" field
            $inputFilter->add([
                    'name'     => 'state',
                    'required' => true,
                    'filters'  => [
                            ['name' => 'StringTrim'],
                    ],
                    'validators' => [
                            ['name'=>'StringLength', 'options'=>['min'=>1, 'max'=>32]]
                    ],
            ]);
            
            // Add input for "post_code" field
            $inputFilter->add([
                    'name'     => 'post_code',
                    'required' => true,
                    'filters'  => [
                    ],
                    'validators' => [
                            ['name' => 'IsInt'],
                            ['name'=>'Between', 'options'=>['min'=>0, 'max'=>999999]]
                    ],
            ]);
            
            // Add input for "country" field
            $inputFilter->add([
                    'name'     => 'country',
                    'required' => false,
                    'filters'  => [
                            ['name' => 'Alpha'],
                            ['name' => 'StringTrim'],
                            ['name' => 'StringToUpper'],
                    ],
                    'validators' => [
                            ['name'=>'StringLength', 'options'=>['min'=>2, 'max'=>2]]
                    ],
            ]);     
            
            
        }        
    }
}

As you can see, we’ve added the 2 steps form with fields and filters into the file (example from the Zend 3 book on GitHub).

A dedicated article on the filter and options values  being in use for the legal status (database interaction) will be linked here soon.

To handle the session, we need to add our new container within the global.php file of our project :


/*..*/

'session_containers' => [
'UserRegistration'
],

Now the fun part with the controller and controller factory where we set up the actions. we define 2 actions, a main one to handle the 2 steps and another one to review the details and submit them to the database.

The Controller

 <?php
namespace MyAuth\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use MyAuth\Entity\User;
use MyAuth\Entity\Company;
use MyAuth\Form\RegistrationForm;

class RegistrationController extends AbstractActionController
{

/**
* Session container.
* @var Zend\Session\Container
*/
private $sessionContainer;

/**
* User manager.
* @var MyAuth\Service\UserManager
*/
private $userManager;

/**
* Company manager.
* @var MyAuth\Service\CompanyManager
*/
private $CompanyManager;
/**
* Company manager.
* @var MyAuth\Service\LegalManager
*/
private $LegalManager;

/**
* Constructor. Its goal is to inject dependencies into controller.
*/
public function __construct($sessionContainer,$userManager,$companyManager,$legalManager)
{
$this->sessionContainer = $sessionContainer;
$this->userManager = $userManager;
$this->companyManager = $companyManager;
$this->legalManager = $legalManager;
}


/**
* This is the default "index" action of the controller. It displays the
* User Registration page.
*/
public function indexAction()
{
// Determine the current step.
$step = 1;
if (isset($this->sessionContainer->step)) {
$step = $this->sessionContainer->step;
}

// Ensure the step is correct (between 1 and 3).
if ($step < 1 || $step > 2)
$step = 1;

if ($step == 1) {
// Init user choices.
$this->sessionContainer->userChoices = [];
}

$form = new RegistrationForm($step,$this->legalManager);

// Check if user has submitted the form
if($this->getRequest()->isPost()) {

// Fill in the form with POST data
$data = $this->params()->fromPost();

$form->setData($data);

// Validate form
if($form->isValid()) {

// Get filtered and validated data
$data = $form->getData();

// Save user choices in session.
$this->sessionContainer->userChoices["step$step"] = $data;

// Increase step
$step ++;
$this->sessionContainer->step = $step;

// If we completed all 2 steps, save data and redirect to Review page.
if ($step > 2) {

// Add the data to the Database now :
$company = $this->companyManager->addCompany($data);

$userdata = $this->sessionContainer->userChoices["step1"];
$userdata['company_id'] = $company->getId();

$user    = $this->userManager->addUser($userdata);


return $this->redirect()->toRoute('registration',
['action'=>'review']);
}

// Go to the next step.
return $this->redirect()->toRoute('registration');
}
}

$viewModel = new ViewModel([
'form' => $form
]);
$viewModel->setTemplate("my-auth/registration/step$step");

return $viewModel;
}

/**
* The "review" action shows a page allowing to review data entered on previous
* 2 steps.
*/
public function reviewAction()
{
// Validate session data.
if(!isset($this->sessionContainer->step) ||
$this->sessionContainer->step <= 2 ||
!isset($this->sessionContainer->userChoices)) {
throw new \Exception('Sorry, the data is not available for review yet');
}

// Retrieve user choices from session.
$userChoices = $this->sessionContainer->userChoices;

return new ViewModel([
'userChoices' => $userChoices
]);
}

} 

Once the second step is filled up, we submit the data to the database using the entity manager and session data stored from the first step.

Following is the factory code to invoke our new container :

 <?php
namespace MyAuth\Controller\Factory;

use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
use MyAuth\Controller\RegistrationController;
use MyAuth\Service\CompanyManager;
use MyAuth\Service\UserManager;
use MyAuth\Service\LegalManager;

/**
* This is the factory for IndexController. Its purpose is to instantiate the
* controller.
*/
class RegistrationControllerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container,
$requestedName, array $options = null)
{
$sessionContainer = $container->get('UserRegistration');
$userManager = $container->get(UserManager::class);
$companyManager = $container->get(CompanyManager::class);
$legalManager = $container->get(LegalManager::class);

// Instantiate the controller and inject dependencies
return new RegistrationController($sessionContainer,$userManager,$companyManager,$legalManager);
}
} 

Before we handle the views, let’s add the Company entity Manager and  its factory :


<?php
namespace MyAuth\Service;

use MyAuth\Entity\Company;
use MyAuth\Entity\Legal;

/**
* This service is responsible for adding/editing users
* and changing user password.
*/
class CompanyManager
{
/**
* Doctrine entity manager.
* @var Doctrine\ORM\EntityManager
*/
private $entityManager;

/**
* Constructs the service.
*/
public function __construct($entityManager)
{
$this->entityManager = $entityManager;
}

/**
* This method adds a new company.
*/
public function addCompany($data)
{
// Do not allow several companies with the same name.
if($this->checkCompanyExists($data['company_name'])) {
throw new \Exception("Company with  name " . $data['company_name'] . " already exists");
}

// Create new Company entity.
$company = new Company();

$company->setCompanyIdentification($data['company_identification']);
$company->setCompanyName($data['company_name']);

/* Transform status integer to an entity */
$company->setStatus($this->getStatus($data['status']));

$currentDate = date('Y-m-d H:i:s');
$company->setDateCreated($currentDate);

// Add the entity to the entity manager.
$this->entityManager->persist($company);

// Apply changes to database.
$this->entityManager->flush();

return $company;
}

/**
* This method updates data of an existing company.
*/
public function updateCompany($company, $data)
{
// Do not allow to change user email if another user with such email already exits.
if($company->getId()!=$data['id'] && $this->checkCompanyExists($data['company_name'])) {
throw new \Exception("Another company with same name " . $data['company_name'] . " already exists");
}

$company->setCompanyName($data['company_name']);
$company->setCompanyIdentification($data['company_identification']);
$company->setStatus($data['status']);

// Apply changes to database.
$this->entityManager->flush();
return true;
}


/**
* Checks whether an active company with given name already exists in the database.
*/
public function checkCompanyExists($name) {

$company = $this->entityManager->getRepository(Company::class)
->findOneByCompanyName($name);

return $company!== null;
}


/**
* Retrieve legal status entity from ID
*/
public function getStatus($id) {

$legal = $this->entityManager->getRepository(Legal::class)
->findOneById($id);

return $legal;
}


}

don’t forget to import the legal entity.


<?php
namespace MyAuth\Service\Factory;

use Interop\Container\ContainerInterface;
use MyAuth\Service\CompanyManager;
/**
* This is the factory class for CompanyManager service. The purpose of the factory
* is to instantiate the service and pass it dependencies (inject dependencies).
*/
class CompanyManagerFactory
{
/**
* This method creates the CompanyManager service and returns its instance.
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$entityManager = $container->get('doctrine.entitymanager.orm_default');

return new CompanyManager($entityManager);
}
}

We will also control the data concerning the legal status to avoid any hacking attempt, thus we need a legal entity manager and its factory :


<?php
namespace MyAuth\Service;

use MyAuth\Entity\Legal;

/**
* This service is responsible for adding/editing legal status
*/
class LegalManager
{
/**
* Doctrine entity manager.
* @var Doctrine\ORM\EntityManager
*/
private $entityManager;

/**
* Constructs the service.
*/
public function __construct($entityManager)
{
$this->entityManager = $entityManager;
}


/**
* Get the list of legal status -- by country ?
*/
public function getLegalStatus() {

$legal = $this->entityManager->getRepository(Legal::class)
->findAll();

$legalarray = [];

// transform the object into an array for the select form
foreach ($legal as $object)
{
$legalarray[$object->getId()] = $object->getStatusName();
}

return $legalarray;
}


}

This class will help us populate the select field on the form at step 2.


<?php
namespace MyAuth\Service\Factory;

use Interop\Container\ContainerInterface;
use MyAuth\Service\LegalManager;
/**
* This is the factory class for LegalManager service. The purpose of the factory
* is to instantiate the service and pass it dependencies (inject dependencies).
*/
class LegalManagerFactory
{
/**
* This method creates the LEgalManager service and returns its instance.
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$entityManager = $container->get('doctrine.entitymanager.orm_default');

return new LegalManager($entityManager);
}
}

And the validator just in case :

 <?php namespace MyAuth\Validator; use Zend\Validator\AbstractValidator; use MyAuth\Entity\Legal; /** * This validator class is designed for checking if there is an existing legal status * with such ID. */ class LegalExistsValidator extends AbstractValidator { /** * Available validator options. * @var array */ protected $options = array( 'entityManager' => null ); // Validation failure message IDs. const NOT_INTEGER  = 'notInteger'; const STATUS_EXISTS = 'statusExists'; /** * Validation failure messages. * @var array */ protected $messageTemplates = array( self::NOT_INTEGER  => "The legal status must be an integer value", self::STATUS_EXISTS=> "Another status with such id does not exist" ); /** * Constructor. */ public function __construct($options = null) { // Set filter options (if provided). if(is_array($options)) { if(isset($options['entityManager'])) $this->options['entityManager'] = $options['entityManager']; } // Call the parent class constructor parent::__construct($options); } /** * Check if status exists. */ public function isValid($value) { if(!is_integer($value)) { $this->error(self::NOT_INTEGER); return false; } // Get Doctrine entity manager. $entityManager = $this->options['entityManager']; $legal = $entityManager->getRepository(Legal::class) ->findOneById($value); if($legal->getId()!=$value && $legal!=null) $isValid = false; else $isValid = true; // If there were an error, set error message. if(!$isValid) { $this->error(self::STATUS_EXISTS); } // Return validation result. return $isValid; } } 

 

While we are with validations, do not forget to load the Phone Validator class within your main Application :


<?php
namespace Application\Validator;
use Zend\Validator\AbstractValidator;
/**
* This validator class is designed for checking a phone number for conformance to
* the local or to the international format.
*/
class PhoneValidator extends AbstractValidator
{
// Phone format constants
const PHONE_FORMAT_LOCAL = 'local'; // Local phone format "333-7777"
const PHONE_FORMAT_INTL  = 'intl';  // International phone format "1 (123) 456-7890"

/**
* Available validator options.
* @var array
*/
protected $options = [
'format'        => self::PHONE_FORMAT_INTL
];

// Validation failure message IDs.
const NOT_SCALAR  = 'notScalar';
const INVALID_FORMAT_INTL  = 'invalidFormatIntl';
const INVALID_FORMAT_LOCAL = 'invalidFormatLocal';

/**
* Validation failure messages.
* @var array
*/
protected $messageTemplates = [
self::NOT_SCALAR  => "The phone number must be a scalar value",
self::INVALID_FORMAT_INTL  => "The phone number must be in international format (e.g. 1 (123) 456-7890)",
self::INVALID_FORMAT_LOCAL => "The phone number must be in local format (e.g. 333-7777)",
];

/**
* Constructor.
* @param string One of PHONE_FORMAT_-prefixed constants.
*/
public function __construct($options = null)
{
// Set filter options (if provided).
if(is_array($options)) {

if(isset($options['format']))
$this->setFormat($options['format']);
}

// Call the parent class constructor
parent::__construct($options);
}

/**
* Sets phone format.
* @param string One of PHONE_FORMAT_-prefixed constants.
*/
public function setFormat($format)
{
// Check input argument.
if($format!=self::PHONE_FORMAT_LOCAL && $format!=self::PHONE_FORMAT_INTL) {
throw new \Exception('Invalid format argument passed.');
}

$this->options['format'] = $format;
}

/**
* Validates a phone number.
* @param string $value User-entered phone number.
* @return boolean true if the number is valid; otherwise false.
*/
public function isValid($value)
{
if(!is_scalar($value)) {
$this->error(self::NOT_SCALAR);
return $false; // Phone number must be a scalar.
}

// Convert the value to string.
$value = (string)$value;

$format = $this->options['format'];

// Determine the correct length and pattern of the phone number,
// depending on the format.
if($format == self::PHONE_FORMAT_INTL) {
$correctLength = 16;
$pattern = '/^\d \(\d{3}\) \d{3}-\d{4}$/';
} else { // self::PHONE_FORMAT_LOCAL
$correctLength = 8;
$pattern = '/^\d{3}-\d{4}$/';
}

// First check phone number length
$isValid = false;
if(strlen($value)==$correctLength) {
// Check if the value matches the pattern
if(preg_match($pattern, $value))
$isValid = true;
}

// If there were an error, set error message.
if(!$isValid) {
if($format==self::PHONE_FORMAT_INTL)
$this->error(self::INVALID_FORMAT_INTL);
else
$this->error(self::INVALID_FORMAT_LOCAL);
}

// Return validation result.
return $isValid;
}
}

 

Depending on the step variable, the controller dispatches the requests to the right view.

Talking about the views, we will add a new directory inside the view/my-auth/ directory and we call it registration.

The view

As we have a 2 steps’ form, we create 2 files :

<?php
$form->get('email')->setAttributes([
        'class'=>'form-control',
        'placeholder'=>'name@yourcompany.com'
]);

$form->get('full_name')->setAttributes([
        'class'=>'form-control',
        'placeholder'=>'John Doe'
]);

$form->get('password')->setAttributes([
        'class'=>'form-control',
        'placeholder'=>'Type password here (6 characters at minimum)'
]);

$form->get('confirm_password')->setAttributes([
        'class'=>'form-control',
        'placeholder'=>'Repeat password'
]);

$form->get('submit')->setAttributes(array('class'=>'btn btn-primary'));

$form->prepare();
?>

<h1>Company &amp; User Registration - Step 1</h1>

<div class="row">
    <div class="col-md-6">
        <?= $this->form()->openTag($form); ?>
        
        <div class="form-group">
            <?= $this->formLabel($form->get('email')); ?>
            <?= $this->formElement($form->get('email')); ?>
            <?= $this->formElementErrors($form->get('email')); ?>
        </div>
        
        <div class="form-group">
            <?= $this->formLabel($form->get('full_name')); ?>
            <?= $this->formElement($form->get('full_name')); ?>
            <?= $this->formElementErrors($form->get('full_name')); ?>
        </div>
        
        <div class="form-group">
            <?= $this->formLabel($form->get('password')); ?>
            <?= $this->formElement($form->get('password')); ?>
            <?= $this->formElementErrors($form->get('password')); ?>
        </div>
        
        <div class="form-group">
            <?= $this->formLabel($form->get('confirm_password')); ?>
            <?= $this->formElement($form->get('confirm_password')); ?>
            <?= $this->formElementErrors($form->get('confirm_password')); ?>
        </div>
        
        <div class="form-group">
        <?= $this->formElement($form->get('submit')); ?>
        </div>
        
        <?= $this->formElement($form->get('csrf')); ?>
        
        <?= $this->form()->closeTag(); ?>
    </div>    
</div>   
<?php
$form->get('phone')->setAttributes([
        'class'=>'form-control',
        'placeholder'=>'Phone number in international format'
]);

$form->get('street_address')->setAttributes([
        'class'=>'form-control',
]);

$form->get('company_name')->setAttributes([
        'class'=>'form-control',
]);
$form->get('company_identification')->setAttributes([
        'class'=>'form-control',
]);
$form->get('city')->setAttributes([
        'class'=>'form-control',
]);

$form->get('state')->setAttributes([
        'class'=>'form-control',
]);

$form->get('post_code')->setAttributes([
        'class'=>'form-control',
]);

$form->get('country')->setAttributes([
        'class'=>'form-control'
]);
$form->get('status')->setAttributes([
        'class'=>'form-control'
]);

$form->get('submit')->setAttributes(array('class'=>'btn btn-primary'));

$form->prepare();
?>

<h1>User Registration - Step 2 - Company Information</h1>

<div class="row">
    <div class="col-md-6">
        <?= $this->form()->openTag($form); ?>
 
         <div class="form-group">
            <?= $this->formLabel($form->get('company_name')); ?>
            <?= $this->formElement($form->get('company_name')); ?>
            <?= $this->formElementErrors($form->get('company_name')); ?>
        </div>
        
        <div class="form-group">
            <?= $this->formLabel($form->get('company_identification')); ?>
            <?= $this->formElement($form->get('company_identification')); ?>
            <?= $this->formElementErrors($form->get('company_identification')); ?>
        </div>    
        <div class="form-group">
            <?= $this->formLabel($form->get('status')); ?>
            <?= $this->formElement($form->get('status')); ?>
            <?= $this->formElementErrors($form->get('status')); ?>
        </div>                     
        <div class="form-group">
            <?= $this->formLabel($form->get('phone')); ?>
            <?= $this->formElement($form->get('phone')); ?>
            <?= $this->formElementErrors($form->get('phone')); ?>
        </div>
        
        <div class="form-group">
            <?= $this->formLabel($form->get('street_address')); ?>
            <?= $this->formElement($form->get('street_address')); ?>
            <?= $this->formElementErrors($form->get('street_address')); ?>
        </div>
        
        <div class="form-group">
            <?= $this->formLabel($form->get('city')); ?>
            <?= $this->formElement($form->get('city')); ?>
            <?= $this->formElementErrors($form->get('city')); ?>
        </div>
        
        <div class="form-group">
            <?= $this->formLabel($form->get('state')); ?>
            <?= $this->formElement($form->get('state')); ?>
            <?= $this->formElementErrors($form->get('state')); ?>
        </div>
        
        <div class="form-group">
            <?= $this->formLabel($form->get('post_code')); ?>
            <?= $this->formElement($form->get('post_code')); ?>
            <?= $this->formElementErrors($form->get('post_code')); ?>
        </div>
        
        <div class="form-group">
            <?= $this->formLabel($form->get('country')); ?>
            <?= $this->formElement($form->get('country')); ?>
            <?= $this->formElementErrors($form->get('country')); ?>
        </div>
        
        <div class="form-group">
        <?= $this->formElement($form->get('submit')); ?>
        </div>
        
        <?= $this->formElement($form->get('csrf')); ?>
        
        <?= $this->form()->closeTag(); ?>
    </div>    
</div>   

Another one is displaying the submitted data, for now we just copy the data from GitHub book :


<h1>User Registration - Review</h1>

<p>Thank you! Now please review the data you entered in previous 2 steps.</p>

<pre>
<?php print_r($userChoices); ?>
</pre>

There are fields which aren’t used inside the database, but you should get the knowledge to do whatever changes you need. If you are stuck at some point, post a comment and I will be glad to answer you !

Final Step :

To get our controller and factory working, let’s add the configuration inside our module.config.php file so we have :


/*..*/

        'controllers' => [
                'factories' => [
                        Controller\AuthController::class => 
                        Controller\Factory\AuthControllerFactory::class,
                        Controller\IndexController::class =>
                        Controller\Factory\IndexControllerFactory::class, 
                        Controller\RegistrationController::class =>
                        Controller\Factory\RegistrationControllerFactory::class, 
                ],
        ],

Inside the same file we need to set up the new routes :


                        'registration' => [
                                'type'    => Segment::class,
                                'options' => [
                                        'route'    => '/registration[/:action]',
                                        'constraints' => [
                                                'action' => '[a-zA-Z][a-zA-Z0-9_-]*'
                                        ],
                                        'defaults' => [
                                                'controller'    => Controller\RegistrationController::class,
                                                'action'        => 'index',
                                        ],
                                ],
                        ],

And the access to them :

         'access_filter' => [
'controllers' => [
Controller\IndexController::class => [
// Give access to "resetPassword", "message" and "setPassword" actions
// to anyone.
['actions' => ['resetPassword', 'message', 'setPassword'], 'allow' => '*'],
// Give access to "index", "add", "edit", "view", "changePassword" actions to authorized users only.
['actions' => ['index', 'add', 'edit', 'view', 'changePassword'], 'allow' => '@']
],
Controller\RegistrationController::class => [
// Give access to registration actions
// to anyone.
['actions' => ['index','review'], 'allow' => '*'],

],

]
], 

And the configuration about the service manager :

'service_manager' => [
'factories' => [
\Zend\Authentication\AuthenticationService::class
=> Service\Factory\AuthenticationServiceFactory::class,
Service\AuthAdapter::class => Service\Factory\AuthAdapterFactory::class,
Service\AuthManager::class => Service\Factory\AuthManagerFactory::class,
Service\UserManager::class => Service\Factory\UserManagerFactory::class,
Service\CompanyManager::class => Service\Factory\CompanyManagerFactory::class,
Service\LegalManager::class => Service\Factory\LegalManagerFactory::class,
],
],

If you have started from the User demo project on GitHub as the current tutorial, we need to clean up the call to the admin user creation, in your UserManager.php file, remove the function : createAdminUserIfNotExists() and remove the call to that function inside AuthController.php.

One more last thing before we implement the database integration, what do you think about adding a Register link to the menu ?

I bet you want it, so here it is inside the NavManager.php from your application module :


/*..*/

// Add the registration menu right after the Sign in items :

$items[] = [
'id' => 'registration',
'label' => 'Register',
'link'  => $url('registration'),
'float' => 'right'
];

Testing the code

Now, get online, it’s time to test, once you click on the Register button, we have this :

Test it out, the second step is :

And the review process :

Now check out the results within your database :

 

I hope things have been clear enough for you, otherwise, hit me up, I can also send you the whole module if you need. But beforehand try your best to make it work, that will be of a good practice.

In next tutorials, we will discuss how to implement the signup via Facebook and other social media.

Stay tuned !

 

Zend references :

Session Management with Zend

Multiple steps form

Filtering

Doctrine references :

Doctrine Association Mapping

Leave a Reply

Want more information?

Related links will be displayed here in this section for you to pick up another good spot to get more details about Web marketing and Search Engine Optimization. There will be some sites which we selected to ease the work of any webmaster or/and web marketer on the Internet.

%d bloggers like this: