• Advertisement

  • 2016-09-29

    Migrating a SVN repository to Git (Bitbucket)

    Migrating a SVN repository to Git (Bitbucket)

    Preface

    This article explains how to migrate a SVN repository to Git. Although this guide uses BitBucket as the Git repository, you can easily adjust the steps to migrate to a different Git repository provider.

    I was recently tasked to migrate a repository from SVN to Git (BitBucket). I have tried the the importer from BitBucket but it failed due to corruption in our SVN repository.

    So I had no other alternative than to do things by hand. Below is the process I have used and some gotchas.

    Authors

    SVN stores just a username with every commit. So nikos could be one and Nikos could be another user. Git however stores also the email of the user and to make things work perfectly we need to create an authors.txt file which contains the mapping between the SVN users and the Git users.

    NOTE The authors.txt file is not necessary for the migration. It only helps for the mapping between your current users (in your Git installation).

    The format of the file is simple:

    captain = Captain America <[email protected]>
    

    If you have the file ready, skip the steps below. Alternatively you can generate the authors.txt file by running the following command in your SVN project folder:

    svn log -q | \
        awk -F '|' '/^r/ {sub("^ ", "", $2); sub(" $", "", $2); print $2" = "$2" <"$2">"}' | \
        sort -u > authors.txt
    

    Conventions

    • The source SVN repository is called SVNSOURCE
    • The target GIT repository is called GITTARGET
    • The SVN URL is https://svn.avengers.org/svn

    Commands

    Create a work folder and cd into it

    mkdir source_repo
    cd source_repo/
    

    Initialize the Git repository and copy the authors file in it

    git svn init https://svn.avengers.org/svn/SVNSOURCE/ --stdlayout 
    cp ../authors.txt .
    

    Set up the authors mapping file in the config

    git config svn.authorsfile authors.txt
    

    Check the config just in case

    git config --local --list
    

    The output should be something like this:

    core.repositoryformatversion=0
    core.filemode=true
    core.bare=false
    core.logallrefupdates=true
    svn-remote.svn.url=https://svn.avengers.org/svn/SVNSOURCE
    svn-remote.svn.fetch=trunk:refs/remotes/trunk
    svn-remote.svn.branches=branches/*:refs/remotes/*
    svn-remote.svn.tags=tags/*:refs/remotes/tags/*
    svn.authorsfile=authors.txt
    

    Get the data from SVN (rerun the command if there is a timeout or proxy error)

    git svn fetch
    

    Check the status of the repository and the branches

    git status
    git branch -a
    

    Create the new bare work folder

    cd ..
    mkdir source_bare
    cd source_bare/
    

    Initialize the bare folder and map the trunk

    git init --bare .
    git symbolic-ref HEAD refs/heads/trunk
    

    Return to the work folder

    cd ..
    cd source_repo/
    

    Add the bare repo as the remote and push the data to it

    git remote add bare ../source_bare/
    git config remote.bare.push 'refs/remotes/*:refs/heads/*'
    git push bare
    

    Return to the bare work folder and check the branches

    cd ..
    cd source_bare/
    git branch
    

    Rename trunk to master

    git branch -m trunk master
    

    Note all the branches that are prefixed /tags/ and modify the lines below (as many times as necessary) to convert SVN tags to Git tags

    git tag 3.0.0 refs/heads/tags/3.0.0
    ...
    git branch -D tags/3.0.0
    ...
    

    Alternatively you can put the following in a script and run it:

    git for-each-ref --format='%(refname)' refs/heads/tags | \
    cut -d / -f 4 | \
    while read ref
    do
      git tag "$ref" "refs/heads/tags/$ref";
      git branch -D "tags/$ref";
    done
    

    Check the branches and the new tags

    git br
    git tags
    

    Check the authors

    git log
    

    Push the repository to BitBucket

    git push --mirror [email protected]:avengers/GITTARGET
    

    Enjoy

  • 2016-07-29

    Phalcon 3.0.0 released

    This article is cross posted from the Phalcon Blog

    Phalcon 3.0.0 final (LTS) released

    The Phalcon team is very excited to share some news with our community!

    The last few months, we have been working hard to push 2.1 out, which contains significant enhancements as well as some API changes that require attention so as not to break compatibility with your application.
    On top of that we have been working in making Zephir PHP7 compatible so that you can enjoy Phalcon in your PHP7 application. Some news first though:

    Versioning

    For any future Phalcon releases we are adopting SemVer (http://semver.org). In short:

    Given a version number MAJOR.MINOR.PATCH, increment the:
    * MAJOR version when you make incompatible API changes,
    * MINOR version when you add functionality in a backwards-compatible manner, and
    * PATCH version when you make backwards-compatible bug fixes.
    * Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.

    Since 2.1 has many API changes, we decided that it would be best to not release it as is and start using SemVer to better communicate with the community and keep track of our releases.

    2.1 is dead, all hail 3.0

    As mentioned above, 2.1 will not be fully backwards compatible. As a result, we are changing the version number to 3.0.

    PHP version support

    The Phalcon team takes security very seriously and thus have decided to provide support to PHP versions that are supported. As of 3.0, PHP 5.3 and 5.4 will be deprecated. We are making a small exception to this rule and will continue to support 5.5 for a little while, but since its support has expired a few days ago, it will too be deprecated in a future release.

    The goodie bag

    So what does 3.0 offer? The changelog is extensive as you can see. Below are highlights of the changes as well as areas you need to concentrate.

    • PHP 5.3 and 5.4 are fully deprecated.
    You can compile the code on your own, but we will not be able to support it nor can we guarantee that it will work as you expect it to. PHP 5.3 support expired mid 2014 and 5.4 expired mid 2015. We need to ensure our applications have all known vulnerabilities on the PHP side fixed and patched, thus we will not support any unsupported PHP version. This excludes PHP 5.5, whose support expired a few days ago. We will deprecate 5.5 in a future release but will make sure that you all know beforehand so that you can prepare.

    INCOMPATIBLE: You will need to upgrade your PHP installation to 5.6. You can always continue to use the Phalcon version you are using, but in 3.0 support for PHP 5.4 has been deprecated and we cannot guarantee that PHP 5.5 will be fully functional.

    APPLICATION

    Phalcon\Cli\Console and Phalcon\Mvc\Application now inherits Phalcon\Application.
    This change makes the interfaces more uniformed and offers additional functionality to the respective applications (cli/mvc)

    BEANSTALK

    • Added \Phalcon\Queue\Beanstalk::ignore().
    Removes the named tube from the watch list for the current connection.

    • Added \Phalcon\Queue\Beanstalk::pauseTube().
    Can delay any new job being reserved for a given time.

    • Added \Phalcon\Queue\Beanstalk::kick().
    It moves jobs into the ready queue. If there are any buried jobs, it will only kick buried jobs. Otherwise it will kick delayed jobs.

    // Kick the job, it should move to the ready queue again
    if (false !== $job->kick()) {
        $job = $this->client->peekReady();
    }
    

    • Added \Phalcon\Queue\Beanstalk::listTubeUsed().
    Returns the tube currently being used by the client.

    • Added \Phalcon\Queue\Beanstalk::listTubesWatched().
    Returns a list tubes currently being watched by the client.

    • Added \Phalcon\Queue\Beanstalk::peekDelayed().
    Return the delayed job with the shortest delay left.

    $this->client->put('testPutInTube', ['delay' => 2]);
    $job = $this->client->peekDelayed();
    

    • Added \Phalcon\Queue\Beanstalk::jobPeek().
    Returns the next available job.

    $this->client->choose(self::TUBE_NAME_1);
    $jobId = $this->client->put('testPutInTube');
    $job   = $this->client->jobPeek($jobId);
    $this->assertEquals($jobId, $job->getId());
    

    CACHE

    • The cache backend adapters now return boolean when calling Phalcon\Cache\BackendInterface::save

    // Returns true/false
    $result = $backendCache->save('my_key', $content);
    

    • Added Phalcon\Cache\Frontend\Msgpack. MsgPack is a new frontend cache. It is an efficient binary serialization format, which allows exchanging data among multiple languages like JSON.

    use Phalcon\Cache\Backend\File;
    use Phalcon\Cache\Frontend\Msgpack;
    
    // Cache the files for 2 days using Msgpack frontend
    $frontCache = new Msgpack(
        [
            'lifetime' => 172800,
        ]
    );
    
    // Create the component that will cache 'Msgpack' to a 'File' backend
    // Set the cache file directory - important to keep the '/' at the end of
    // of the value for the folder
    $cache = new File(
        $frontCache, 
        [
            'cacheDir' => '../app/cache/',
        ]
    );
    
    // Try to get cached records
    $cacheKey = 'robots_order_id.cache';
    $robots   = $cache->get($cacheKey);
    
    if ($robots === null) {
        // $robots is null due to cache expiration or data do not exist
        // Make the database call and populate the variable
        $robots = Robots::find(['order' => 'id']);
    
        // Store it in the cache
        $cache->save($cacheKey, $robots);
    }
    
    // Use $robots
    foreach ($robots as $robot) {
        echo $robot->name, "\n";
    }
    

    • Fixed bug of destroy method of Phalcon\Session\Adapter\Libmemcached

    • Added Phalcon\Cache\Backend\Memcache::addServers to enable pool of servers for memcache

    $memcache->addServers('10.4.6.10', 11000, true);
    $memcache->addServers('10.4.6.11', 11000, true);
    $memcache->addServers('10.4.6.12', 11000, true);
    

    CRYPT

    • Mcrypt is replaced with openssl in Phalcon\Crypt #11530
    Due to the lack of updates for mcrypt for a number of years, its slow performance and the fact that the PHP core team decided to deprecate mcrypt as soon as possible (version 7.1 onward), we have replaced it with the much faster and supported openssl.

    • Default encrypt algorithm in Phalcon\Crypt is now changed to AES-256-CFB

    • Removed methods setMode(), getMode(), getAvailableModes() in Phalcon\CryptInterface (no longer apply with openssl)

    BACKWARDS INCOMPATIBLE: Backwards compatibility from openssl to mcrypt is problematic if not impossible. We had to remove several methods that are no longer applicable. Additionally the rijndael-256 from mcrypt is no longer valid in openssl. The default encryption algorithm is AES-256-CFB

    If you have data that has already been encrypted with mcrypt, you will need first to decrypt it before upgrading to 3.0 and then encrypt it again using 3.0 and therefore openssl. Failure to do so will result in loss of data. A port is available in the incubator. Please see the code here

    DATABASE

    • Dropped support of Oracle #12009
    Support of Oracle has been dropped from the Phalcon Core for the following reasons:
    •• The lack of Oracle maintainer
    •• The lack of relevant experience among the Phalcon Core Team
    •• Weak support or interest from the community
    •• Incomplete implementation that creates only the illusion of support for Oracle
    •• Some issues hampering for the support of PHP 7 in Phalcon

    Oracle components will be ported to the Phalcon Incubator. If the adapter receives support and enhancements from the community, we will consider making it part of the core again.

    DI

    Phalcon\Di is now bound to services closures allowing use Phalcon\Di as $this to access services within them. Additionally, closures used as handlers inMvc\Micro are now bound to the $app instance

    Old way:

    $diContainer->setShared(
        'modelsCache',
        function () use ($config) {
            $frontend = '\Phalcon\Cache\Frontend\\' . $config->get('modelsCache')->frontend;
            $frontend = new $frontend(
                [
                    'lifetime' => $config->get('modelsCache')->lifetime,
                ]
            );
            $config   = $config->get('modelsCache')->toArray();
            $backend  = '\Phalcon\Cache\Backend\\' . $config['backend'];
    
            return new $backend($frontend, $config);
        }
    );
    

    New way:

    $diContainer->setShared(
        'modelsCache',
        function () {
            $frontend = '\Phalcon\Cache\Frontend\\' . $this->config->get('modelsCache')->frontend;
            $frontend = new $frontend(
                [
                    'lifetime' => $this->config->get('modelsCache')->lifetime,
                ]
            );
            $config   = $this->config->get('modelsCache')->toArray();
            $backend  = '\Phalcon\Cache\Backend\\' . $config['backend'];
    
            return new $backend($frontend, $config);
        }
    );
    

    Also note the nested DI behavior:

    $foo = function() {
        get_class($this); // DI
        $bar = function () {
            get_class($this); // DI
            $baz = function () {
                // etc
            }
        }
    }
    

    • If an object is returned after firing the event beforeServiceResolve in Phalcon\Di it overrides the default service localization process

    DISPATCHER

    • Added Phalcon\Dispatcher::hasParam().

    public function testAction() 
    {    
        if (true === $this->dispatcher->hasParam('foo')) {
            // Parameter exists
        }
    }
    

    • Added method getActionSuffix() in Phalcon\DispatcherInterface. This allows you change the 'Action' suffix in controller actions.

    • Corrected behavior to fire the dispatch:beforeException event when there is any exception during dispatching

    • CLI parameters are now handled consistently.

    • Added Phalcon\Mvc\Controller\BindModelInterface and associated model type hint loading through dispatcher.

    • Added Phalcon\Mvc\Collection::update, Phalcon\Mvc\Collection::create and Phalcon\Mvc\Collection::createIfNotExist

    public function createAction() 
    {
        /**
         * Creates a document based on the values in the attributes, if not found by criteria
         */
        $robot = new Robot();
        $robot->name = 'MyRobot';
        $robot->type = 'Droid';
        $robot->create();
    }
    
    public function createOverrideAction() 
    {
        /**
         * Create a document
         */
        $robot = new Robot();
        $robot->name = 'MyRobot';
        $robot->type = 'Droid';
        //create only if robot with same name and type does not exist
        $robot->createIfNotExist( array( 'name', 'type' ) );
    }
    
    public function updateAction() 
    {
        /**
         * Update a document
         */
        $robot = Robots::findFirst(['id' => 1]);
        $robot->name = 'MyRobot';
        $robot->type = 'Droid';
        $robot->update();
    }
    

    EVENTS

    • Now Phalcon\Events\Event implements Phalcon\Events\EventInterface

    Phalcon\Events\Event::getCancelable renamed to Phalcon\Events\Event::isCancelable

    BACKWARDS INCOMPATIBLE: Any references to getCancelable will stop working. You will need to rename the function to isCancelable

    Old way:

    public function cancelAction()
    {
        if (true === $this->eventsManager->getCancelable()) {
            // do something here
        }
    }
    

    New way:

    public function cancelAction()
    {
        if (true === $this->eventsManager->isCancelable()) {
            // do something here
        }
    }
    

    • Removed Phalcon\Events\Manager::dettachAll in favor of Phalcon\Events\Manager::detachAll

    BACKWARDS INCOMPATIBLE: Any references to dettachAll will stop working. You will need to rename the function to detachAll

    Old way:

    public function destroyAction()
    {
        $this->eventsManager->dettachAll()
    }
    

    New way:

    public function destroyAction()
    {
        $this->eventsManager->detachAll()
    }
    

    FLASH

    • Added ability to autoescape Flash messages

    $flash = new Phalcon\Flash\Session;
    $flash->setEscaperService(new Phalcon\Escaper);
    
    $flash->success("<script>alert('This will execute as JavaScript!')</script>");
    echo $flash->output();
    // <div class="successMessage"><script>alert('This will execute as JavaScript!')</script></div>
    

    • Fixed Phalcon\Session\Flash::getMessages.
    Now it returns an empty array in case of non existent message type request

    Old result:

    use Phalcon\Session\Flash as FlashSession;
    
    $flash = new FlashSession();
    $flash->error('Error Message');
    var_dump($flash->getMessages('success', false));
    
    array (size=1)
      'error' => 
        array (size=1)
          0 => string 'Error Message' (length=13)
    

    New result:

    use Phalcon\Session\Flash as FlashSession;
    
    $flash = new FlashSession();
    $flash->error('Error Message');
    var_dump($flash->getMessages('success', false));
    
    array (size=0)
      empty
    

    HTTP REQUEST/RESPONSE

    • Added default header: Content-Type: "application/json; charset=UTF-8" in method Phalcon\Http\Response::setJsonContent

    Old way:

    use Phalcon\Http\Response;
    
    $data     = 'Phlying with Phalcon';
    $response = new Response();
    $response->setContentType('application/json;');
    $response->setJsonContent($data)
    $response->send();
    

    New way:

    $data     = 'Phlying with Phalcon';
    $response = new Response();
    $response->setJsonContent($data)
    $response->send();
    

    • Added ability to spoof the HTTP request method.
    Most browsers do not support sending PUT and DELETE requests via the method attribute in an HTML form. If the X-HTTP-Method-Override header is set, and if the method is a POST, then it is used to determine the 'real' intended HTTP method. The _method request parameter can also be used to determine the HTTP method, but only if setHttpMethodParameterOverride(true) has been called. By including a _method parameter in the query string or parameters of an HTTP request, Phalcon will use this as the method when matching routes. Forms automatically include a hidden field for this parameter if their submission method is not GET or POST.

    • Added support of CONNECT, TRACE and PURGE HTTP methods.
    - CONNECT: A variation of HTTP tunneling when the originating request is behind a HTTP proxy server. With this mechanism, the client first requests the HTTP proxy server to forward the TCP connection to the final endpoint. The HTTP proxy server then establishes the connection on behalf of the client.
    - TRACE: A method used for debugging which echoes input back to the user. Note that this method is dangerous, since it introduces a risk whereby an attacker could steal information such as cookies and possibly server credentials.
    - PURGE: Although not defined in the HTTP RFCs, some HTTP servers and caching systems implement this method and use it to purge cached data.

    • Refactored Phalcon\Http\Request::getHttpHost.
    Now it always returns the hostname or empty an string. Optionally validates and cleans host name #11921

    • Renamed Phalcon\Http\Request::isSoapRequest to Phalcon\Http\Request::isSoap and Phalcon\Http\Request::isSecureRequest to Phalcon\Http\Request::isSecure.
    Left the originals functions as aliases and marked them deprecated.

    CAUTION: Any references to isSoapRequest need to be renamed to isSoap. Any references to isSecureRequest need to be renamed to isSecure.

    Old way:

    public function testAction()
    {
        if (true === $this->request->isSoapRequest()) {
            //
        }
    
        if (true === $this->request->isSecureRequest()) {
            //
        }
    }
    

    New way:

    public function testAction()
    {
        if (true === $this->request->isSoap()) {
            //
        }
    
        if (true === $this->request->isSecure()) {
            //
        }
    }
    

    • Added Phalcon\Http\Request::setStrictHostCheck and Phalcon\Http\Request::isStrictHostCheck to manage strict validation of the host name.

    use Phalcon\Http\Request;
    
    $request = new Request;
    
    $_SERVER['HTTP_HOST'] = 'example.com';
    $request->getHttpHost(); // example.com
    
    $_SERVER['HTTP_HOST'] = 'example.com:8080';
    $request->getHttpHost(); // example.com:8080
    
    $request->setStrictHostCheck(true);
    $_SERVER['HTTP_HOST'] = 'ex=am~ple.com';
    $request->getHttpHost(); // UnexpectedValueException
    
    $_SERVER['HTTP_HOST'] = 'ExAmPlE.com';
    $request->getHttpHost(); // example.com
    

    • Added Phalcon\Http\Request::getPort.
    Returns the port on which the request is made i.e. 80, 8080, 443 etc.

    • Added setLastModified method to Phalcon\Http\Response
    Sets the Last-Modified header

    public function headerAction()
    {
        $this->response->setLastModified(new DateTime());
    }
    

    • Add setContentLength method to Phalcon\Http\Response
    Sets the response content-length

    public function headerAction()
    {
        $this->response->setContentLength(2048);
    }
    

    LOADER

    • Removed support for prefixes strategy in Phalcon\Loader

    BACKWARDS INCOMPATIBLE: In Phalcon 2, you could load classes using a specific prefix. This method was very popular before namespaces were introduced. For instance:

    <?php
    
    use Phalcon\Loader;
    
    // Creates the autoloader
    $loader = new Loader();
    
    // Register some prefixes
    $loader->registerPrefixes(
        array(
            "Example_Base"    => "vendor/example/base/",
            "Example_Adapter" => "vendor/example/adapter/",
            "Example_"        => "vendor/example/"
        )
    );
    
    // Register autoloader
    $loader->register();
    
    // The required class will automatically include the
    // file vendor/example/adapter/Some.php
    $some = new Example_Adapter_Some();
    

    This functionality is no longer supported

    • Added \Phalcon\Loader::registerFiles and \Phalcon\Loader::getFiles. registerFiles registers files that are "non-classes" hence need a "require". This is very useful for including files that only have functions. getFiles
    returns the files currently registered in the autoloader

    $loader->registerFiles(
        [
            'fuctions.php',
            'arrayFunctions.php',
        ]
    );
    

    MODELS

    • Changed constructor of Phalcon\Mvc\Model to allow pass an array of initialization data

    $customer = new Customer(
        [
            'Name'   => 'Peter',
            'Status' => 'active',
        ]
    );
    $customer->save();
    

    Phalcon\Mvc\Model now implements JsonSerializable making easy serialize model instances

    $customers = Customers::find();
    echo json_encode($customers); // {['id':1,...],['id':2,...], ...}
    

    Phalcon\Mvc\Model\Criteria::getOrder renamed to Phalcon\Mvc\Model\Criteria::getOrderBy

    BACKWARDS INCOMPATIBLE: Any references to getOrder will stop working. You will need to rename the function to getOrderBy

    • Added method getOption() in Phalcon\Mvc\Model\RelationInterface
    Returns an option by the specified name. If the option does not exist null is returned

    • Added OR operator for Phalcon\Mvc\Model\Query\Builder methods: betweenWhere, notBetweenWhere, inWhere and notInWhere

    $builder->betweenWhere('price', 100.25, 200.50);     // Appends a BETWEEN condition
    $builder->notBetweenWhere('price', 100.25, 200.50);  // Appends a NOT BETWEEN condition
    $builder->inWhere('id', [1, 2, 3]);                  // Appends an IN condition
    $builder->notInWhere('id', [1, 2, 3]);               // Appends an NOT IN condition
    

    • Added new getter Phalcon\Mvc\Model\Query\Builder::getJoins()
    Returns the join parts from query builder

    • When destructing a Mvc\Model\Manager PHQL cache is cleaned

    • Added FULLTEXT index type to Phalcon\Db\Adapter\Pdo\Mysql

    • Fixed afterFetch event not being sent to behaviors

    • Fixed issue with Model::__set that was bypassing setters

    • Fixed issue with Model::__set setting hidden attributes directly when setters are not declared

    Phalcon\Mvc\Model\Manager::load() now can load models from aliased namespaces

    Phalcon\Mvc\Model\Transaction\Manager now correctly keeps account of transactions

    Phalcon\Db\Dialect\Sqlite now maps additional column types to SQLite columns equivalents.

    • Fixed Phalcon\Mvc\Model\Resultset::update() - Removed endless loop queries

    • Fixed Phalcon\Mvc\Model\Manager::_mergeFindParameters - Merging conditions fix

    ROLES

    • Added Phalcon\Acl\RoleAware and Phalcon\Acl\ResourceAware Interfaces. Now you can pass objects to Phalcon\Acl\AdapterInterface::isAllowed as roleName and resourceName, also they will be automatically passed to function defined in Phalcon\Acl\AdapterInterface::allow or Phalcon\Acl\AdapterInterface::deny by type

    use UserRole;       // Class implementing RoleAware interface
    use ModelResource;  // Class implementing ResourceAware interface
    
    // Set access level for role into resources
    $acl->allow('Guests', 'Customers', 'search');
    $acl->allow('Guests', 'Customers', 'create');
    $acl->deny('Guests', 'Customers', 'update');
    
    // Create our objects providing roleName and resourceName
    $customer     = new ModelResource(1, 'Customers', 2);
    $designer     = new UserRole(1, 'Designers');
    $guest        = new UserRole(2, 'Guests');
    $anotherGuest = new UserRole(3, 'Guests');
    
    // Check whether our user objects have access to the operation on model object
    $acl->isAllowed($designer, $customer, 'search')     // Returns false
    $acl->isAllowed($guest, $customer, 'search')        // Returns true
    $acl->isAllowed($anotherGuest, $customer, 'search') // Returns true
    

    Phalcon\Acl\AdapterInterface::allow and Phalcon\Acl\AdapterInterface::deny have 4th argument - function. It will be called when using Phalcon\Acl\AdapterInterface::isAllowed

    Phalcon\Acl\AdapterInterface::isAllowed have 4th argument - parameters. You can pass arguments for a function defined in Phalcon\Acl\AdapterInterface:allow or Phalcon\Acl\AdapterInterface::deny as associative array where key is argument name

    // Set access level for role into resources with custom function
    $acl->allow(
        'Guests', 
        'Customers', 
        'search',
        function ($a) {
            return $a % 2 == 0;
        }
    );
    
    // Check whether role has access to the operation with custom function
    $acl->isAllowed('Guests', 'Customers', 'search', ['a' => 4]); // Returns true
    $acl->isAllowed('Guests', 'Customers', 'search', ['a' => 3]); // Returns false
    

    • Fixed wildcard inheritance in Phalcon\Acl\Adapter\Memory #12006

    use Phalcon\Acl;
    use Phalcon\Acl\Adapter\Memory as MemoryAcl;
    
    $acl = new MemoryAcl();
    
    $acl->setDefaultAction(Acl::DENY);
    
    $roleGuest      = new Role("guest");
    $roleUser       = new Role("user");
    $roleAdmin      = new Role("admin");
    $roleSuperAdmin = new Role("superadmin");
    
    $acl->addRole($roleGuest);
    $acl->addRole($roleUser, $roleGuest);
    $acl->addRole($roleAdmin, $roleUser);
    $acl->addRole($roleSuperAdmin, $roleAdmin);
    
    $acl->addResource("payment", ["paypal", "facebook",]);
    
    $acl->allow($roleGuest->getName(), "payment", "paypal");
    $acl->allow($roleGuest->getName(), "payment", "facebook");
    
    $acl->allow($roleUser->getName(), "payment", "*");
    
    echo $acl->isAllowed($roleUser->getName(), "payment", "notSet");  // true
    echo $acl->isAllowed($roleUser->getName(), "payment", "*");       // true
    echo $acl->isAllowed($roleAdmin->getName(), "payment", "notSet"); // true
    echo $acl->isAllowed($roleAdmin->getName(), "payment", "*");      // true
    

    ROUTES

    • Routes now can have an associated callback that can override the default dispatcher + view behavior

    • Amended Phalcon\Mvc\RouterInterface and Phalcon\Mvc\Router. Added missed addPurge, addTrace and addConnect methods.
    Added addConnect for the CONNECT HTTP method, addPurge for the PURGE HTTP method and addTrace for the TRACE HTTP method

    • Placeholders :controller and :action in Mvc\Router now defaults to /([\\w0-9\_\-]+) instead of /([\\a-zA-Z0-9\_\-]+)

    • Modifier #u (PCRE_UTF8) is now default in regex based routes in Mvc\Router

    Mvc\Router\Route now escapes characters such as . or + to avoid unexpected behaviors

    • Fixed the use of the annotation router with namespaced controllers

    • Fixed matching host name by Phalcon\Mvc\Route::handle when using port on current host name

    SECURITY

    • Added Phalcon\Security::hasLibreSsl and Phalcon\Security::getSslVersionNumber
    Mostly these are used internally but can be used to get information about libreSsl.

    • Changed default hash algorithm in Phalcon\Security to CRYPT_BLOWFISH_Y

    Phalcon\Security is using now Phalcon\Security\Random

    • Enforced that Phalcon\Security::getToken() and Phalcon\Security::getTokenKey() return a random value per request not per call

    Phalcon\Security::getToken() and Phalcon\Security::getTokenKey() are using now Phalcon\Security::_numberBytes instead of passed as argument or hard coded value

    Phalcon\Security::hash() corrected not working CRYPTSTDDES, CRYPTEXTDES, MD5, CRYPT_SHA256

    Phalcon\Security::hash() CRYPT_SHA512 fixed wrong salt length

    • Added missing unit-tests for Phalcon\Security

    SESSION

    • Removed Phalcon\Session

    BACKWARDS INCOMPATIBLE: Any references to Phalcon\Session have to be removed and replaced with the relevant adapter class

    • Fixed the Session write callback

    TEXT

    • Added ability to use custom delimiter for Phalcon\Text::camelize and Phalcon\Text::uncamelize

    use Phalcon\Text;
    
    public function displayAction()
    {
        echo Text::camelize('c+a+m+e+l+i+z+e', '+'); // CAMELIZE
    }
    

    • Fixed Phalcon\Text:dynamic() to allow custom separator

    VIEW

    • An absolute path can now be used to Mvc\View::setLayoutsDir
    You can now use one layout path for all the landing pages of your application for instance, even from separate projects

    • Now Phalcon\Mvc\View supports many views directories at the same time

    • Return false from an action disables the view component (same as $this->view->disable())

    public function displayAction()
    {
        // Do some stuff here
    
        return false; // Same as $this->view->disable();
    }
    

    • Return a string from an action takes it as the body of the response

    • Return a string from an Mvc\Micro handler takes it as the body of the response

    public function displayAction()
    {
        // Do some stuff here
    
        // $this->response->setContent('<h1>Hello World</h1>');
        return '<h1>Hello World</h1>';
    }
    

    • Fixed odd view behavior related to setLayout() and pick()

    VALIDATION

    Phalcon\Mvc\Model\Validation is now deprecated in favor of Phalcon\Validation
    The functionality of both components is merged into one, allowing us to reduce the codebase while offering the same functionality as before.

    Old way:

    namespace Invo\Models;
    
    use Phalcon\Mvc\Model;
    use Phalcon\Mvc\Model\Validator\Email as EmailValidator;
    use Phalcon\Mvc\Model\Validator\Uniqueness as UniquenessValidator;
    
    class Users extends Model
    {
        public function validation()
        {
            $this->validate(
                new EmailValidator(
                    [
                        'field' => 'email',
                    ]
                )
            );
    
            $this->validate(
                new UniquenessValidator(
                    [
                        'field'   => 'username',
                        'message' => 'Sorry, That username is already taken',
                    ]
                )
            );
    
            if ($this->validationHasFailed() == true) {
                return false;
            }
        }
    }
    

    New way:

    namespace Invo\Models;
    
    use Phalcon\Mvc\Model;
    use Phalcon\Validation;
    use Phalcon\Validation\Validator\Email as EmailValidator;
    use Phalcon\Validation\Validator\Uniqueness as UniquenessValidator;
    
    class Users extends Model
    {
        public function validation()
        {
            $validator = new Validation();
    
            $validator->add(
                'email', //your field name
                new EmailValidator([
                    'model' => $this,
                    'message' => 'Please enter a correct email address'
                ])
            );
    
            $validator->add(
                'username',
                new UniquenessValidator([
                    'model' => $this,
                    'message' => 'Sorry, That username is already taken',
                ])
            );
    
            return $this->validate($validator);
        }
    }
    

    • Method isSetOption in Phalcon\Validation\ValidatorInterface marked as deprecated, please use hasOption

    CAUTION: Any references to isSetOption need to be renamed to hasOption

    Old way:

    if (true === $validation->isSetOption('my-option')) {
        //
    }
    

    New way:

    if (true === $validation->hasOption('my-option')) {
        //
    }
    

    • Added internal check allowEmpty before calling a validator. If it option is true and the value of empty, the validator is skipped

    • Added option to validate multiple fields with one validator (fix uniqueness validator as well), also removes unnecessary model => $this in Phalcon\Validation\Validator\Uniqueness.

    Phalcon\Validation\Validator\Alpha now correctly validates non-ASCII characters

    • Added Phalcon\Validation\CombinedFieldsValidator, validation will pass array of fields to this validator if needed

    Phalcon\Validation\Validator\Digit now correctly validates digits

    use Phalcon\Validation\Validator\Digit as DigitValidator;
    
    $validator->add(
        'height', 
        new DigitValidator(
            [
                'message' => ':field must be numeric',
            ]
        )
    );
    
    $validator->add(
        [
            'height', 
            'width',
        ], 
        new DigitValidator(
            [
                'message' => [
                    'height' => 'height must be numeric',
                    'width'  => 'width must be numeric',
                ]
            ]
        )
    );
    

    • Added Phalcon\Validation\Validator\Date

    use Phalcon\Validation\Validator\Date as DateValidator;
    
    $validator->add(
        'date', 
        new DateValidator(
            [
                'format'  => 'd-m-Y',
                'message' => 'The date is not valid',
            ]
        )
    );
    
    $validator->add(
        [
            'date',
            'anotherDate',
        ], 
        new DateValidator(
            [
                'format'  => [
                    'date'        => 'd-m-Y',
                    'anotherDate' => 'Y-m-d',
                ],
                'message' => [
                    'date'        => 'The date is invalid',
                    'anotherDate' => 'The another date is invalid',
                ]
            ]
        )
    );
    

    • Fixed Phalcon\Validation::appendMessage to allow append message to the empty stack

    • Added convert option to the Phalcon\Validation\Validator\Uniqueness to convert values to the database lookup #12030

    use Phalcon\Validation\Validator\Uniqueness;
    
    $validator->add(
        'username', 
        new Uniqueness(
            [
                'convert' => function (array $values) {
                    $values['username'] = strtolower($values['username']);
    
                    return $values;
                }
            ]
        )
    );
    

    INTERFACES

    • Removed __construct from all interfaces #11441

    • Added Phalcon\Cli\DispatcherInterface, Phalcon\Cli\TaskInterface, Phalcon\Cli\RouterInterface and Phalcon\Cli\Router\RouteInterface.

    DOCUMENTATION

    • Added Indonesian translation #840

    VARIOUS

    • Added Phalcon\Assets\Manager::exists() to check if collection exists

    • Fixed Filter::add method handler

    • Fixed issue with radio not being checked when default value is 0

    • Phalcon\Tag::getTitle() shows a title depending on prependTitle and appendTitle

    • Using a settable variable for the Mongo Connection Service name instead of a hard coded string

    Phalcon\Debug\Dump skip debugging di, fix detecting private/protected properties

    • Added new setter Phalcon\Escaper::setDoubleEncode() - to allow setting/disabling double encoding

    • Fixed Phalcon\Config::merge for working with php7

    PHP7

    Phalcon 3.0 supports PHP7! In subsequent releases we will focus on the development of the framework to implove the compatibility and take advantage of the performance enhancements that PHP7 offers. You can install the framework in php7 using the usual installation instructions.

    Support

    Phalcon 3.0 Long Term Support (LTS) version is out, and it’s packed with new features to help you better create web applications with PHP. This version of the framework will be maintained for 3 years from now.

    Acknowledgments

    We want to greatly thank everyone who has contributed to accomplish and achieve the completion of this release. Special thanks to our friends around the world that have made possible this release:

    Conclusion

    Phalcon 3.0 takes a step forward towards a modern framework for PHP. We'll continue working making it more useful and performant for developers. Thank you once more to our wonderful community and users!

    Installation

    You can install Phalcon 3.0 for either PHP 5.5/5.6/7.0 using the following instructions:

    git clone --depth=5 https://github.com/phalcon/cphalcon
    cd cphalcon/build
    sudo ./install
    

    Windows DLLs are available in the download page.

    As always, many thanks to everyone involved in this release and thanks for choosing Phalcon!

    <3 Phalcon Team

  • 2015-10-29 21:15:00

    Setting up AWS RDS MySQL replication offsite

    Preface

    Recently I worked on setting up replication between our AWS RDS instance and a server running as a MySQL slave in our location. Although the task was not difficult, there are quite a few areas that one needs to pay attention to.

    In this blog post, I am going to outline the process step by step, so that others can benefit and not lose time trying to discover what went wrong.

    Disclaimer: Google, DuckDuckGo, the AWS forums and this blog post have been invaluable guides to help me do what I needed to do.

    Setup

    • One RDS MySQL or Aurora instance (Master)
    • One server running MySQL in your premises (or wherever you want to put it) (Slave)
    • Appropriate access to IP of the Slave on the master.

    Master Setup

    There is little to do on our master (RDS). Depending on the database size and update frequency, we will need to set up the maximum retention time for the bin logs. For a very large database we need to set a high number, so that we are able to export the database from the master, import it in the slave and start replication.

    Connect to your database and run the following command:

    MySQL [(none)]> call mysql.rds_set_configuration('binlog retention hours', 24);
    

    You can use a different number of hours; I am using 24 for this example.

    Slave Setup

    I am assuming that MySQL is installed on the machine that has been designated as the slave, and also that that machine has ample space for the actual data as well as the binary logs that will be created for the replication.

    Edit my.cnf

    The location of this file is usually under /etc or /etc/mysql. Depending on your distribution it might be located elsewhere.

    [mysqld]
    ...
    
    #bind-address = 0.0.0.0
    
    # Logging and Replication
    general_log_file  = /logs/mysql.log
    general_log       = 1
    log_error         = /logs/mysql_safe.log
    log_slow_queries  = /logs/mysql-slow.log
    long_query_time   = 2
    slave-skip-errors = 1062
    log-queries-not-using-indexes
    
    server-id         = 1234567
    log_bin           = /logs/mysql-bin.log
    expire_logs_days  = 2
    max_binlog_size   = 100M
    

    Note: The configuration file will contain a lot more entries but the ones above are the ones you need to pay attention to.

    • bind-address: We need to comment this line so that we can connect to the instance from somewhere else in the network. Keep this line if you are going to work only on the slave machine and allow no connections from elsewhere.
    • general_log_file: The location of your query log file. You can disable this (see next entry) but it is always good to keep it on at least at the start, to ensure that replication is moving smoothly. Tailing that log will give you a nice indicator of the activity in your database.
    • general_log: Enable or disable the general log
    • log_error: Where to store the errors log
    • log_slow_queries: Where to store the slow queries log. Especially helpful in identifying bottlenecks in your application
    • long_query_time: Time to specify what a slow query is
    • slave-skip-errors: 1062 is the "1062 | Error 'Duplicate entry 'xyz' for key 1' on query. Default database: 'db'. Query: 'INSERT INTO ...'" error. Helpful especially when the replication starts.
    • log-queries-not-using-indexes: We want this because it can help identifying potential bottlenecks in the application
    • server-id: A unique ID for your slave instance.
    • log_bin: Where the binary replication logs are kept
    • expire_logs_days: How long to keep the replication logs for
    • max_binlog_size: Maximum replication log size (per file)

    Once you set these up, restart your MySQL instance

    /etc/init.d/mysql restart
    
    Download the SSH Public Key for RDS

    In your slave server, navigate to /etc/mysql and download the rds-combined-ca-bundle.pem file. This file will be used by the slave to ensure that all the replication traffic is done using SSL and nobody can eavesdrop on your data in transit.

    cd /etc/mysql
    wget http://s3.amazonaws.com/rds-downloads/rds-combined-ca-bundle.pem
    

    NOTE You can put the rds-combined-ca-bundle.pem anywhere on your slave. If you change the path, you will have to modify the command to connect the slave to the master (shown further below) to specify the exact location of the key.

    Import timezone data

    This step might not be necessary depending on your MySQL installation. However since RDS works with UTC, you might find your replication breaking because your slave MySQL instance cannot understand the UTC timezone. The shell command you need to run on your slave machine to fix this is:

    mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql
    
    Creating the RDS related tables

    RDS uses its own tables to keep track of the replication status and other related data such as the replication heartbeat, configuration etc. Those tables need to be present in the mysql database of your slave in order for the replication to work.

    DROP TABLE IF EXISTS `rds_configuration`;
    CREATE TABLE `rds_configuration` (
      `name` varchar(100) NOT NULL,
      `value` varchar(100) DEFAULT NULL,
      `description` varchar(300) NOT NULL,
      PRIMARY KEY (`name`)
    ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
    
    DROP TABLE IF EXISTS `rds_global_status_history`;
    CREATE TABLE `rds_global_status_history` (
      `collection_end` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
      `collection_start` timestamp NULL DEFAULT NULL,
      `variable_name` varchar(64) NOT NULL,
      `variable_value` varchar(1024) NOT NULL,
      `variable_delta` int(20) NOT NULL,
      PRIMARY KEY (`collection_end`,`variable_name`)
    ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
    
    DROP TABLE IF EXISTS `rds_global_status_history_old`;
    CREATE TABLE `rds_global_status_history_old` (
      `collection_end` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
      `collection_start` timestamp NULL DEFAULT NULL,
      `variable_name` varchar(64) NOT NULL,
      `variable_value` varchar(1024) NOT NULL,
      `variable_delta` int(20) NOT NULL,
      PRIMARY KEY (`collection_end`,`variable_name`)
    ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
    
    DROP TABLE IF EXISTS `rds_heartbeat2`;
    CREATE TABLE `rds_heartbeat2` (
      `id` int(11) NOT NULL,
      `value` bigint(20) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
    
    DROP TABLE IF EXISTS `rds_history`;
    CREATE TABLE `rds_history` (
      `action_timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      `called_by_user` varchar(50) NOT NULL,
      `action` varchar(20) NOT NULL,
      `mysql_version` varchar(50) NOT NULL,
      `master_host` varchar(255) DEFAULT NULL,
      `master_port` int(11) DEFAULT NULL,
      `master_user` varchar(16) DEFAULT NULL,
      `master_log_file` varchar(50) DEFAULT NULL,
      `master_log_pos` mediumtext,
      `master_ssl` tinyint(1) DEFAULT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
    
    DROP TABLE IF EXISTS `rds_replication_status`;
    CREATE TABLE `rds_replication_status` (
      `action_timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      `called_by_user` varchar(50) NOT NULL,
      `action` varchar(20) NOT NULL,
      `mysql_version` varchar(50) NOT NULL,
      `master_host` varchar(255) DEFAULT NULL,
      `master_port` int(11) DEFAULT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
    
    DROP TABLE IF EXISTS `rds_sysinfo`;
    CREATE TABLE `rds_sysinfo` (
      `name` varchar(25) DEFAULT NULL,
      `value` varchar(50) DEFAULT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
    

    NOTE I am not 100% that all of these tables are needed. I have seen only the rds_heartbeat2 and rds_replication_status used. You can experiment with these when you enable replication and add each table in turn if needed. You can confirm whether the above are correct for your instance by connecting to the master and taking a mysqldump of the mysql database.

    Replication

    Replication user

    We need to create a user in our master database that will have the appropriate rights to perform all the replication related actions. We need these commands to be run on the master. For this example I am creating a user called rpluser with the password 424242:

    MySQL [(none)]> CREATE USER 'rpluser'@'%' IDENTIFIED BY '424242';
    MySQL [(none)]> GRANT REPLICATION SLAVE ON *.* TO 'rpluser'@'%';
    
    Master Status

    Connect to your master and issue this command:

    MySQL [(none)]> show master status;
    

    The output will be something like this:

    +----------------------------+----------+--------------+------------------+-------------------+
    | File                       | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
    +----------------------------+----------+--------------+------------------+-------------------+
    | mysql-bin-changelog.000123 |   171819 |              |                  |                   |
    +----------------------------+----------+--------------+------------------+-------------------+
    

    Keep those values handy (File and Position) since we will use them to instruct the slave where to start requesting data from the master (binlog file and position).

    mysqldump

    Take a database dump of all the databases in RDS (exclude information_schema and mysql). If your database can afford a bit of downtime you can use the --opt flag in mysqldump, which will lock all tables until the backup completes. If not, you can use the --skip-add-locks flag. More information about mysqldump options can be found here

    mysqldump --host=192.168.1.2 --user='root' --password my_db > /backups/my_db.sql
    

    Adjust the above command to fit your needs. Once all databases have been dumped, we need to import them in the slave.

    Importing data in the slave

    Navigate to the folder you have all the *.sql dump files, connect to the slave database and start sourcing them.

    cd /backups
    mysql --host=192.168.1.2 --user='root' --password
    MySQL [(none)]> create database my_db;
    MySQL [(none)]> use my_db;
    MySQL [my_db]> source my_db.sql;
    

    Repeat the process of creating the database, using it and sourcing the dump file until all your databases have been imported.

    NOTE There are other ways of doing the above, piping the results directly to the database or even using RDS to get the data straight from it without a mysqldump. Whichever way you choose is up to you. In my experience, the direct import worked for a bit until our database grew to a point that it was timing out or breaking while importing, so I opted for the multi step approach. Have a look at this section in the AWS RDS documentation for more options.

    Connecting to the master

    Once your restore has been completed it is time to connect our slave to the master. In order to connect to the master from the slave we need to verify the following:

    • The name of the RDS instance (for the command below I will use myinstance.rds.amazonaws.com)
    • The name of the replication user (we chose rpluser)
    • The password of the replication user (we chose 424242)
    • The master log file (see above, we got mysql-bin-changelog.000123 from show master status;)
    • The master log file position (see above, we got 171819)
    • The location of the SSL certificate (we used /etc/mysql/rds-combined-ca-bundle.pem)

    The command we need to run on the slave MySQL server is (newlines added for readability):

    MySQL [(none)]> CHANGE MASTER TO 
        -> MASTER_HOST='myinstance.rds.amazonaws.com', 
        -> MASTER_USER='rpluser', 
        -> MASTER_PASSWORD='424242', 
        -> MASTER_LOG_FILE='mysql-bin-changelog.000123', 
        -> MASTER_LOG_POS=171819, 
        -> MASTER_SSL=1, 
        -> MASTER_SSL_CERT='', 
        -> MASTER_SSL_CA='/etc/mysql/rds-combined-ca-bundle.pem', 
        -> MASTER_SSL_KEY='';
    
    Starting the replication

    All we have to do now is to start the slave:

    MySQL [(none)]> START SLAVE;
    

    We can check if everything is OK either by using the general log (see my.cnf section) by tailing it from the shell:

    tail -f /logs/mysql.log
    

    or by issuing this command on the mysql prompt:

    MySQL [(none)]> SHOW SLAVE STATUS \G;
    *************************** 1. row ***************************
                   Slave_IO_State: Waiting for master to send event
                      Master_Host: myinstance.rds.amazonaws.com
                      Master_User: rpluser
                      Master_Port: 3306
                    Connect_Retry: 60
                  Master_Log_File: mysql-bin-changelog.000123
              Read_Master_Log_Pos: 171819
                   Relay_Log_File: mysqld-relay-bin.000002
                    Relay_Log_Pos: 123
    ...
                       Last_Errno: 0
                       Last_Error: 
    ...
               Master_SSL_Allowed: Yes
               Master_SSL_CA_File: /etc/mysql/rds-combined-ca-bundle.pem
               Master_SSL_CA_Path: 
                  Master_SSL_Cert: 
                Master_SSL_Cipher: 
                   Master_SSL_Key: 
    ...
          Slave_SQL_Running_State: Slave has read all relay log; waiting for the slave I/O thread to update it
    ...
    

    Congratulations on your working RDS slave (offsite) machine :)

    Conclusion

    This blog post is by no means exhausting all the topics that replication can cover. For additional information please see the references below.

    I hope you find this post helpful :)

    References

  • 2015-10-18 13:09:00

    Using Traits to add more functionality to your classes in PHP

    Traits are a mechanism for code reuse in single inheritance languages such as PHP.

    A Trait is similar to a class, but only intended to group functionality in a fine-grained and consistent way. It is not possible to instantiate a Trait on its own. It is an addition to traditional inheritance and enables horizontal composition of behavior; that is, the application of class members without requiring inheritance. Source

    Traits have been introduced in PHP 5.4.0. However, a lot of developers have not yet embraced them and taken advantage of the power that they offer.

    As mentioned above in the snippet of the PHP manual, Traits are a mechanism to reuse code, making your code more DRY.

    Let's have a look at a real life example of how Traits can help you with your Phalcon project, or any project you might have.

    Models

    With Phalcon, we have model classes which represent pretty much a table in our database, and allows us to interact with a record or a resultset for the needs of our application.

    Scenario

    We have an application where we need to store information about Companies. Each Company can have one or more Customers as well as one or more Employees. We chose to store that information in three different tables.

    For each Employee or Customer, we need to store their first name, middle name and last name. However we also need to be able to show the full name in this format:

    <Last name>, <First Name> <Middle Name>
    

    Using custom getter

    In each model we can use a custom getter method in the Phalcon model to calculate the full name of the record.

    Employee
    namespace NDN\Models;
    
    class Employee
    {
        ...
        public function getFullName()
        {
            return trim(
                sprintf(
                    '%s, %s %s',
                    $this->getLastName(),
                    $this->getFirstName(),
                    $this->getMiddleName()
                )
            );
        }
    }
    
    Customer
    namespace NDN\Models;
    
    class Customer
    {
        ...
        public function getFullName()
        {
            return trim(
                sprintf(
                    '%s, %s %s',
                    $this->getLastName(),
                    $this->getFirstName(),
                    $this->getMiddleName()
                )
            );
        }
    }
    

    The above introduces a problem. If we want to change the behavior of the getFullName we will have to visit both models and make changes to the relevant methods in each model. In addition, we are using the same code in two different files i.e. duplicating code and effort.

    We could create a base model class that our Customer and Employee models extend and put the getFullName function in there. However that increases the class extensions and could lead to maintenance nightmares.

    For instance we will have to create the base model class that only Customer and Employee models extend but what would happen if we need common functionality for other models? We will need to then create another base model class and so on and so forth. If we end up piling all the common functionality into one base model class then we will end up with functions that would not apply to all of our models and thus a maintenance nightmare.

    NOTE: We can also use the afterFetch method to create a calculated field which will be available for us to use. We can use either the getter or the afterFetch like so:

    namespace NDN\Models;
    
    class Customer
    {
        ...
        public function afterFetch()
        {
            $this->full_name = trim(
                sprintf(
                    '%s, %s %s',
                    $this->getLastName(),
                    $this->getFirstName(),
                    $this->getMiddleName()
                )
            );
        }
    }
    

    Traits

    We can use a trait to offer the same functionality, keeping our code DRY. Since a Trait is not a class that can be instantiated by itself, we attach it to wherever we need to, in this case the Employee and Customer models.

    namespace NDN\Traits;
    
    trait FullNameTrait
    {
        /**
         * Gets the user first/last/med name and formats it in a readable format
         *
         * @return  string
         */
        public function getFullName()
        {
            return trim(
                sprintf(
                    '%s, %s %s',
                    $this->getLastName(),
                    $this->getFirstName(),
                    $this->getMiddleName()
                )
            );
        }
    }
    

    We can attach now this trait to the relevant models

    Employee
    namespace NDN\Models;
    
    use NDN\Traits\FullNameTrait;
    
    class Employee
    {
        use FullNameTrait;
    }
    
    Customer
    namespace NDN\Models;
    
    use NDN\Traits\FullNameTrait;
    
    class Customer
    {
        use FullNameTrait;
    }
    

    Now we can use the getFullName() function in our two models to get the full name of the Employee or Customer calculated by the relevant model fields.

    // Customer:
    // first_name:  John
    // middle_name: Mark
    // last_name:   Doe
    
    // Prints: Doe, John Mark
    echo $customer->getFullName();
    
    // Employee:
    // first_name:  Stanley
    // middle_name: Martin
    // last_name:   Lieber
    
    // Prints: Lieber, Stanley Martin
    echo $employee->getFullName();
    

    Conclusion

    Traits can be very powerful and helpful allies, keeping our code very flexible and reusable.

    Give it a try!

  • 2015-10-04 13:26:00

    Building the Phalcon Blog (i)

    This is the first of a series of posts, describing how we built the Phalcon Blog (and this one of course). The intention is to showcase some of the features of Phalcon and discuss the reasons behind implementing the code in such a way. I will amend this post with the links of the future posts once I post them.

    These series will focus initially on the Phalcon blog (Github) and will then expand on this blog (Github). In the very near future all the features available in this blog will be available in the Phalcon one :)

    As I mentioned in a previous post, Andres and I were not 100% satisfied with Tumblr, the blogging platform that we have used for a few years for the purposes of the Phalcon blog. So we decided that it would not only be beneficial for us to build something of our own, but also for the community, since the software is open sourced and available for everyone to use.

    Bootstrapping process

    In this post I am going to concentrate on bootstrapping the application. By bootstrapping I do not mean using the Bootstrap open source library, despite the probably misleading image on the right.

    Bootstrapping is the class (in our case) that handles pretty much everything that our application needs to run prior to executing actions. This entails

    • Conditional execution between the normal app and the CLI one
    • Application Paths
    • Configuration Files
    • Loader (and composer autoloader) setup
    • Error handling
    • Routes
    • Dispatcher
    • Url
    • Views
      • Main View
      • Simple View (for emails, RSS/Sitemap etc.)
    • Cache
    • Utils
    • Post Finder class

    Some of the above components are also registered in the DI container for further use in the application.

    Implementation

    In other applications we have open sourced such as Vokuro, we have pretty much always included a couple of files in our index.php; one for the loader and one for the services as demonstrated here.

    <?php
    
    error_reporting(E_ALL);
    
    try {
        /**
         * Define some useful constants
         */
        define('BASE_DIR', dirname(__DIR__));
        define('APP_DIR', BASE_DIR . '/app');
        /**
         * Read the configuration
         */
        $config = include APP_DIR . '/config/config.php';
        /**
         * Read auto-loader
         */
        include APP_DIR . '/config/loader.php';
        /**
         * Read services
         */
        include APP_DIR . '/config/services.php';
        /**
         * Handle the request
         */
        $application = new \Phalcon\Mvc\Application($di);
        echo $application->handle()->getContent();
    } catch (Exception $e) {
        echo $e->getMessage(), '<br>';
        echo nl2br(htmlentities($e->getTraceAsString()));
    }
    

    There is nothing wrong with the above approach. We did however consider the fact that the particular index.php file has 3 different file inclusions and if we wanted to tinker with the setup of the application we would have to open all three.

    We opted for one file containing all of our services and application bootstrap. In addition to that, we altered the design so that later on we can add a CLI application without much effort and heavy refactoring.

    NOTE: The CLI application has been implemented on this blog and will very soon be merged to the Phalcon repository. We will cover that functionality in a future post.

    index.php

    Having one file that performs all the necessary initialization tasks a.k.a. bootstrapping our application allows us to have a much smaller index.php file. (comments removed to preserve space)

    <?php
    
    use \Phalcon\Di\FactoryDefault as PhDI;
    use \Kitsune\Bootstrap;
    
    error_reporting(E_ALL);
    
    try {
        require_once '../library/Kitsune/Bootstrap.php';
    
        $di = new PhDI();
        $bootstrap = new Bootstrap();
    
        echo $bootstrap->run($di, []);
    } catch (\Exception $e) {
        if ($di->has('logger')) {
            $logger = $di->getShared('logger');
            $logger->error($e->getMessage());
            $logger->error('<pre>' . $e->getTraceAsString() . '</pre>');
        }
    }
    

    We create a new bootstrap application and pass in it a DI container. For this part of the application the FactoryDefault DI container is used. However we will be able to inject a Phalcon CLI DI container for the CLI application we will discuss later on.

    Bootstrap.php

    Our bootstrap class contains all the code we need to run the application. It is a bit shy of 400 lines which according to PHP Mess Detector is not something we want to be doing because it increases complexity and if we are not careful it will create a mess :). We opted to ignore that rule and left the file as is because once we had everything working as we wanted, we were not going to be messing with that file again.

    Constants

    We use several constants throughout the application.

    • K_PATH - the top folder path of our installation
    • K_CLI - whether this is a CLI application or not
    • K_DEBUG - whether we are in debug/development mode. In this mode all volt templates are being created at every request and cache is not used.
    • K_TESTS - whether we are running the test suite or not (test suite is not implemented yet)
            /**
             * The app path
             */
            if (!defined('K_PATH')) {
                define('K_PATH', dirname(dirname(dirname(__FILE__))));
            }
    
            ....
    
            /**
             * Check if this is a CLI app or not
             */
            $cli   = $utils->fetch($options, 'cli', false);
            if (!defined('K_CLI')) {
                define('K_CLI', $cli);
            }
    
            $tests = $utils->fetch($options, 'tests', false);
            if (!defined('K_TESTS')) {
                define('K_TESTS', $tests);
            }
    
            ....
    
            /**
             * Check if we are in debug/dev mode
             */
            if (!defined('K_DEBUG')) {
                $debugMode = boolval($utils->fetch($config, 'debugMode', false));
                define('K_DEBUG', $debugMode);
            }
    
    Configuration

    The configuration is split into two files. The git tracked base.php (under /var/config/) contains an array of elements that are needed throughout the application, such as cache settings, routes etc. The config.php located in the same folder is installation dependent and is not tracked in git. You can override every element that exists in base.php.

            /**
             * The configuration is split into two different files. The first one
             * is the base configuration. The second one is machine/installation
             * specific.
             */
            if (!file_exists(K_PATH . '/var/config/base.php')) {
                throw new \Exception('Base configuration files are missing');
            }
    
            if (!file_exists(K_PATH . '/var/config/config.php')) {
                throw new \Exception('Configuration files are missing');
            }
    
            /**
             * Get the config files and merge them
             */
            $base     = require(K_PATH . '/var/config/base.php');
            $specific = require(K_PATH . '/var/config/config.php');
            $combined = array_replace_recursive($base, $specific);
    
            $config = new Config($combined);
            $di->set('config', $config, true);
    
    Loader

    The loader uses the namespaces defined in the base.php and config.php. Additionally the composer autoloader is included to offer functionality needed from the composer components we have.

            /**
             * We're a registering a set of directories taken from the
             * configuration file
             */
            $loader = new Loader();
            $loader->registerNamespaces($config->namespaces->toArray());
            $loader->register();
    
            require K_PATH . '/vendor/autoload.php';
    
    Logger

    The logger is set to create a log file every day (with the date as the prefix).

            /**
             * LOGGER
             *
             * The essential logging service
             */
            $format    = '[%date%][%type%] %message%';
            $name      = K_PATH . '/var/log/' . date('Y-m-d') . '-kitsune.log';
            $logger    = new LoggerFile($name);
            $formatter = new LoggerFormatter($format);
            $logger->setFormatter($formatter);
            $di->set('logger', $logger, true);
    
    Error handler

    We decided to have no errors thrown in the application even if those are E_NOTICE. A simple isset() in most cases is more than enough to ensure that there are no E_NOTICE errors thrown in our log. Any errors thrown in the log files slow our application down, even if the errors are suppressed using the php.ini directives. We also set the timezone to US/Eastern in that file. That particular piece could become configurable and stored in the config.php. Finally we specify a custom error handler, to offer verbosity in errors thrown as well as log metrics when in debug mode.

            /**
             * ERROR HANDLING
             */
            ini_set('display_errors', boolval(K_DEBUG));
    
            error_reporting(E_ALL);
    
            set_error_handler(
                function ($exception) use ($logger) {
                    if ($exception instanceof \Exception) {
                        $logger->error($exception->__toString());
                    } else {
                        $logger->error(json_encode(debug_backtrace()));
                    }
                }
            );
    
            set_exception_handler(
                function (\Exception $exception) use ($logger) {
                    $logger->error($exception->getMessage());
                }
            );
    
            register_shutdown_function(
                function () use ($logger, $memoryUsage, $currentTime) {
                    $memoryUsed = number_format(
                        (memory_get_usage() - $memoryUsage) / 1024,
                        3
                    );
                    $executionTime = number_format(
                        (microtime(true) - $currentTime),
                        4
                    );
                    if (K_DEBUG) {
                        $logger->info(
                            'Shutdown completed [Memory: ' . $memoryUsed . 'Kb] ' .
                            '[Execution: ' . $executionTime .']'
                        );
                    }
                }
            );
    
            $timezone = $config->get('app_timezone', 'US/Eastern');
            date_default_timezone_set($timezone);
    
    Routes

    Our routes are stored in the base.php. Additional routes can be set in the config.php. The router is not initialized if this is a CLI application.

            /**
             * Routes
             */
            if (!K_CLI) {
                $di->set(
                    'router',
                    function () use ($config) {
                        $router = new Router(false);
                        $router->removeExtraSlashes(true);
                        $routes = $config->routes->toArray();
                        foreach ($routes as $pattern => $options) {
                            $router->add($pattern, $options);
                        }
    
                        return $router;
                    },
                    true
                );
            }
    
    Dispatcher

    The dispatcher is instantiated with a listener, attaching to the beforeException event of the dispatcher. A custom plugin NotFoundPlugin is used to send output to the 404 page. Using the plugin allows us to reuse it anywhere in the application. This implementation is very beneficial when developing multi module applications.

    NOTE: For the CLI application later on, we will need the CLI dispatcher.

            /**
             * We register the events manager
             */
            $di->set(
                'dispatcher',
                function () use ($di) {
                    $eventsManager = new EventsManager;
    
                    /**
                     * Handle exceptions and not-found exceptions using NotFoundPlugin
                     */
                    $eventsManager->attach('dispatch:beforeException', new NotFoundPlugin);
    
                    $dispatcher = new Dispatcher;
                    $dispatcher->setEventsManager($eventsManager);
    
                    $dispatcher->setDefaultNamespace('Kitsune\Controllers');
    
                    return $dispatcher;
                }
            );
    
    Views

    The views are being initialized using Volt as the template engine. The main view is set up with the expected options.

            $di->set(
                'view',
                function () use ($config) {
                    $view = new View();
                    $view->setViewsDir(K_PATH . '/app/views/');
                    $view->registerEngines([".volt" => 'volt']);
                    return $view;
                }
            );
    
            /**
             * Setting up volt
             */
            $di->set(
                'volt',
                function ($view, $di) {
                    $volt = new VoltEngine($view, $di);
                    $volt->setOptions(
                        [
                            "compiledPath"  => K_PATH . '/var/cache/volt/',
                            'stat'          => K_DEBUG,
                            'compileAlways' => K_DEBUG,
                        ]
                    );
                    return $volt;
                },
                true
            );
    
    Cache

    The cache component is configured using the config.php. We can define the parameters in that file and thus use say the File cache for our local/development machine and a more advanced cache (Memcached for instance) for the production system.

            /**
             * Cache
             */
            $frontConfig = $config->cache_data->front->toArray();
            $backConfig  = $config->cache_data->back->toArray();
            $class       = '\Phalcon\Cache\Frontend\\' . $frontConfig['adapter'];
            $frontCache  = new $class($frontConfig['params']);
            $class       = '\Phalcon\Cache\Backend\\' . $backConfig['adapter'];
            $cache       = new $class($frontCache, $backConfig['params']);
            $di->set('cache', $cache, true);
    
            /**
             * viewCache
             */
            $frontConfig = $config->cache_view->front->toArray();
            $backConfig  = $config->cache_view->back->toArray();
            $class       = '\Phalcon\Cache\Frontend\\' . $frontConfig['adapter'];
            $frontCache  = new $class($frontConfig['params']);
            $class       = '\Phalcon\Cache\Backend\\' . $backConfig['adapter'];
            $cacheView   = new $class($frontCache, $backConfig['params']);
            $di->set('viewCache', $cacheView, true);
    
    Markdown Renderer

    We use Ciconia for the rendering of markdown with several plugins, existing and user defined. The registration is pretty straight forward.

            /**
             * Markdown renderer
             */
            $di->set(
                'markdown',
                function () {
                    $ciconia = new Ciconia();
                    $ciconia->addExtension(new FencedCodeBlockExtension());
                    $ciconia->addExtension(new TaskListExtension());
                    $ciconia->addExtension(new InlineStyleExtension());
                    $ciconia->addExtension(new WhiteSpaceExtension());
                    $ciconia->addExtension(new TableExtension());
                    $ciconia->addExtension(new UrlAutoLinkExtension());
                    $ciconia->addExtension(new MentionExtension());
    
                    $extension = new IssueExtension();
                    $extension->setIssueUrl(
                        '[#%s](https://github.com/phalcon/cphalcon/issues/%s)'
                    );
                    $ciconia->addExtension($extension);
    
                    $extension = new PullRequestExtension();
                    $extension->setIssueUrl(
                        '[#%s](https://github.com/phalcon/cphalcon/pull/%s)'
                    );
                    $ciconia->addExtension($extension);
                    return $ciconia;
                },
                true
            );
    
    Posts Finder

    This is a class we came up with, which is used to give us an easy way to get information about a specific post, the tag cloud, the index page etc. It is utilizing cache a lot!

            /**
             * Posts Finder
             */
            $di->set(
                'finder',
                function () use ($utils, $cache) {
                    $key        = 'post.finder.cache';
                    $postFinder = $utils->cacheGet($key);
                    if (null === $postFinder) {
                        $postFinder = new PostFinder();
                        $cache->save($key, $postFinder);
                    }
                    return $postFinder;
                },
                true
            );
    

    Conclusion

    In the next post of these series we will take a look at the router and discuss what each route means to our application.

    Comments are more than welcome. If you have any questions on the implementation, feel free to ask in the comments below.

    References

  • 2015-10-02 22:42:39

    Every microsecond counts!

    Preface

    One of the primary factors that always needs to be taken into consideration when designing and implementing an application is performance. In this day and age of information overload, everything is about speed. If your website is slow (more than a second or two) to appear on the browser, most likely your visitors will leave and go elsewhere. If the application you designed/implemented is slow, it will use more resources (memory/cpu) and thus cost more money to run. Time is money.

    The Nanosecond

    I have the highest admiration for Grace Hopper, a pioneer computer scientist who invented the first compiler for a computer language and paved the way for the evolution of programming languages in general. In a video she describes the value of a nanosecond in computer terms. I encourage you to click the link and watch it, it might give you a better perspective on why your application must be as fast as possible.

    This Blog

    As I wrote in a previous post, a new version of this blog has been launched, based on work done for for the Phalcon blog. While in development mode, metrics that are printed in the logger. The output looks something like this:

    [Fri, 02 Oct 15 20:10:27 -0400][INFO] Shutdown completed [2.798s] - [4,134.16 KB] 
    [Fri, 02 Oct 15 20:11:01 -0400][INFO] Shutdown completed [2.979s] - [4,134.00 KB] 
    [Fri, 02 Oct 15 20:14:43 -0400][INFO] Shutdown completed [2.891s] - [4,142.60 KB] 
    [Fri, 02 Oct 15 20:26:10 -0400][INFO] Shutdown completed [2.721s] - [1,075.02 KB] 
    [Fri, 02 Oct 15 20:30:16 -0400][INFO] Shutdown completed [2.735s] - [1,002.25 KB] 
    [Fri, 02 Oct 15 20:30:29 -0400][INFO] Shutdown completed [2.708s] - [1,002.29 KB] 
    [Fri, 02 Oct 15 20:54:04 -0400][INFO] Shutdown completed [2.674s] - [1,003.43 KB] 
    [Fri, 02 Oct 15 20:55:28 -0400][INFO] Shutdown completed [2.677s] - [1,003.31 KB] 
    [Fri, 02 Oct 15 21:12:33 -0400][INFO] Shutdown completed [2.013s] - [913.81 KB] 
    [Fri, 02 Oct 15 21:14:11 -0400][INFO] Shutdown completed [2.002s] - [895.35 KB] 
    [Fri, 02 Oct 15 21:32:48 -0400][INFO] Shutdown completed [2.054s] - [894.71 KB] 
    [Fri, 02 Oct 15 21:39:02 -0400][INFO] Shutdown completed [2.028s] - [894.04 KB]
    [Fri, 02 Oct 15 21:44:19 -0400][INFO] Shutdown completed [2.046s] - [895.59 KB]
    [Fri, 02 Oct 15 21:45:55 -0400][INFO] Shutdown completed [2.023s] - [893.75 KB]
    

    As you can see there is room for improvement. Granted these results come from my local installation, where the debugMode is set to 1, which means that there is no caching and everything gets recalculated on every request. Still, if I can make this local installation perform as fast as possible, then on the production server it will be even faster.

    The first few lines show a relatively OK response (2.7-3.0 seconds) but a high usage in memory. This had to be rectified and looking at the code, I managed to refactor the PostFinder class and reduce the memory consumption significantly. Removing objects and referenced objects in them made a huge difference. Arrays work just fine for my purposes.

    Additional optimizations led to dropping the execution time to just above 2.0 seconds and the memory consumption below 1Mb.

    There are still a lot of things I can try (and will) both on the application level as well as the server level. I am aiming to reduce the execution time to below 1 second and I will for sure share the results and the tools/techniques used.

    I hope you enjoy the performance increase (however noticeable). More to come in the near future.

    References

  • 2015-09-28 20:06:28

    New Blog (again), new look, hopefully more posts

    The excuse

    It has definitely been a very long time since I last blogged. Just like my previous hiatus, a lot of personal issues arose that would not allow me the luxury to devote time for this blog.

    Things managed to settle down for a bit, so hopefully I can start posting more frequently.

    New look

    For those that have been following this blog (all 3 of you), you will notice that the look and feel has changed significantly. I decided that I needed a fresh look so after evaluating many platforms and blog related software, I opted for the in house version.

    Many of you know that I have been heavily involved in the Phalcon Framework, where I serve as a member of the core team. For the blogging needs of the project, we were using Tumblr but were not 100% satisfied. So a couple months back we decided to write our own blogging software based on Phalcon!

    The results can be seen on our blog, which is also open sourced on Github.

    For my own purposes I modified the codebase, adding new functionality and modifying it somewhat for my needs. The changes are also on Github and will soon be propagated to the Phalcon repository for everyone to use. This software requires some libraries loaded by composer and of course Phalcon loaded on your web server. The content is served using Markdown.

    As far as the look is concerned, I tried (really did) but nothing came close to what I wanted. Since I am not a CSS developer by any means, I purchased this template by the guys that created Bootstrap. The result IMHO is really nice, responsive and clean. I hope you like it too!

    Future

    I intend on posting more content in the next few weeks. The goal is to post one article (minimum) per week. Other than technology, I am going to focus on Phalcon related posts, sharing experiences, tips and tricks.

    Missing...

    This blog software is still under construction. I have a few things that I need to address such as:

    • Pagination
    • RSS feeds
    • Posts by tag
    • Posts by archive
    • Comments - disqus integration
    • Tests!!!!

    Some of those are already in place but I need to make sure that they work as expected

    I hope you like the new look and stay tuned for more content.

  • 2013-09-15 12:00:00

    Let the RDBMS do more than just store data

    One of the common "mistakes" that programmers (and have been guilty as charged many a times in the past) is not to use the tools that are available to them to the maximum extent possible.

    A common example is using the RDBMS of your choice to only store and retrieve data, without taking advantage of its power and its features to the full extent.

    A RDBMS can do much, much more. One can use triggers that can auto update fields (as I will demonstrate in this blog post), log data into tables, trigger cascade deletes etc.; stored procedures can compute complex data sets, joining tables and transforming data; views can offer easier representations of data, hiding complex queries from the actual application. In addition, such features, like stored procedures/views, can offer security enhancements as well as maintainability to an application. Execution for instance can be restricted to particular groups/logins, while changing the stored procedure/view only requires a change on the database layer and not the application itself.

    In this blog post I will show you a simple example on how one can transfer some of the processing of an application to the RDBMS. I am using MariaDB as the RDBMS and PhalconPHP as the PHP framework.

    The RDBMS

    Each table of my database has several common fields that are used for logging and reporting as well as recording status.

    An example table is as follows

    CREATE TABLE IF NOT EXISTS co_address (
      id             int(11)      unsigned NOT NULL AUTO_INCREMENT,
      address_line_1 varchar(150) COLLATE utf8_unicode_ci DEFAULT NULL,
      address_line_2 varchar(150) COLLATE utf8_unicode_ci DEFAULT NULL,
      address_line_3 varchar(150) COLLATE utf8_unicode_ci DEFAULT NULL,
      region         varchar(6)   COLLATE utf8_unicode_ci DEFAULT NULL,
      post_code      varchar(24)  COLLATE utf8_unicode_ci DEFAULT NULL,
      country        varchar(2)   COLLATE utf8_unicode_ci DEFAULT NULL,
      created_id     int(11)      unsigned NOT NULL DEFAULT '0',
      created_date   datetime              NOT NULL,
      updated_id     int(11)      unsigned NOT NULL DEFAULT '0',
      updated_date   datetime              NOT NULL,
      deleted        tinyint(1)   unsigned NOT NULL DEFAULT '0',
      PRIMARY KEY (id),
      KEY created_id (created_id),
      KEY created_date (created_date),
      KEY updated_id (updated_id),
      KEY updated_date (updated_date),
      KEY deleted (deleted)
    ) ENGINE=InnoDB  
    DEFAULT CHARSET=utf8
    COLLATE=utf8_unicode_ci
    COMMENT='Holds addresses for various entities' AUTO_INCREMENT=1 ;
    

    The fields are:

    Field Name Description
    created_id The id of the user that created the record
    created_date The date/time that the record was created
    updated_id The id of the user that last updated the record
    updated_date The date/time that the record was last updated
    deleted A soft delete flag

    There is not much I can do with the user ids (created/updated) or the deleted column (see also notes below regarding this). However as far as the dates are concerned I can definitely let MariaDB handle those updates.

    Triggers

    The work is delegated to triggers, attached to each table.

    --
    -- Triggers address
    --
    DROP TRIGGER IF EXISTS trg_created_date;
    DELIMITER //
    CREATE TRIGGER trg_created_date BEFORE INSERT ON address
     FOR EACH ROW SET NEW.created_date = NOW(), NEW.updated_date = NOW()
    //
    DELIMITER ;
    DROP TRIGGER IF EXISTS trg_updated_date;
    DELIMITER //
    CREATE TRIGGER trg_updated_date BEFORE UPDATE ON address
     FOR EACH ROW SET NEW.updated_date = NOW()
    //
    DELIMITER ;
    

    The triggers above update the created_date and updated_date fields automatically upon insert/update.

    Phalcon Model

    I needed to make some changes to my model Address, in order to allow the triggers to work without interference from the model.

    class Model extends PhModel
    {
        public function initialize()
        {
            // Disable literals
            $this->setup(['phqlLiterals' => false]);
    
            // We skip these since they are handled by the RDBMS
            $this->skipAttributes(
                [
                    'created_date',
                    'updated_date',
                ]
            );
    
        }
    
        public function getSource()
        {
            return 'address';
        }
    
        public function getCreatedDate()
        {
            return $this->created_date;
        }
    
        public function getUpdatedDate()
        {
            return $this->updated_date;
        }
    }
    

    By using skipAttributes, I am instructing the Phalcon model not to update those fields. By doing so, I am letting my triggers worry about that data.

    Conclusion

    It might seem a very trivial task that I am delegating but in the grand scheme of things, the models of an application can be very complex and have a lot of logic in them (and so might controllers). Delegating some of that logic in the RDBMS simplifies things and also increases performance of the application, which now requires just a bit less computational power.

    NOTES

    For a soft delete feature i.e. automatically updating the deleted field when a DELETE is called, a trigger will not work. Instead one can use a stored procedure for it. See this Stack Overflow answer.

  • 2013-06-25 12:00:00

    Voting for Phalcon as a cPanel feature woes

    Those that have been following my blog and posts on Google+ know that for the last year or so I have been involved heavily in PhalconPHP, a C based PHP framework, which delivers its functionality as an extension loaded on the web server.

    I was honored a few months ago when I became a member of the Phalcon Team and have since tried my best to evangelize Phalcon and to help as much as possible with scheduling NFRs for development, helping in the forum, unit tests, blog posts etc.

    One of the most difficult issues that Phalcon is facing is helping developers installing it on their machines. This of course does not mean that it is really difficult to install. The installation is basically three commands (or if you are on a Windows box you just download the DLL, add the relevant directive in the php.ini and restart the web server).

    However since a lot of people are using shared hosting, they do not have access to the command line where sudo or su are available so that that Phalcon can be installed. This is left to the hosting company and some are very reluctant to install anything at all. I used to own a hosting company and I can assure you that it is indeed a hassle if a handful of clients ask for a library or an installation that is not part of the "norm". You have to maintain it, you have to ensure that it will not interfere with other packages on the server or hinder other clients that reside on the same hosting box.

    A few months ago I approached cPanel through their ticketing system, in an effort to make Phalcon an option for the extensions that can be installed and loaded through their EasyApache application.

    At the time I was granted a development account for the software so that I can try and create an installation script and also was prompted to go to http://features.cpanel.net/ and open a new feature request regarding this (the request is here). The purpose of this exercise is for cPanel to get a feel of what features the community needs and address them. In my communications with them I received the following (emphasis mine):

    Our EasyApache has multiple locations to add includes, or post hooks to compile third party libraries/software during a build, or after a build has completed. That being said, the extensions that we ship to be configured are maintained by the PHP & PECL groups. To answer the question as far as integration on all servers running cPanel, I would recommend creating a feature request for this, and allowing the community to vote for this:

    http://features.cpanel.net/

    And so I did. I created the feature request and we also advertised this in our community via our forum and our blog post.

    The feature received well over 180 votes, making it the third most requested feature in cPanel. Also if you follow the link, one of the engineers of cPanel requested additional information which was provided by myself and others.

    As time went by, I visited the feature and requested an update but never got a reply back. More and more votes kept on coming in so it was really a waiting game at that time.

    All of a sudden though a week ago, one of our users in the forum asked in our Forum why the feature request in cPanel has only 7 votes. This came to us as a surprise so I went and checked it out. Lo and behold the vote counter was at 7 and not at 180+. Assuming that this was some sort of a glitch, I opened a ticket with cPanel and inquired about this.

    A short time later I received a reply from a Vice President of Operations stating (emphasis mine):

    Our feature request system was designed for features requests from our customers and in reviewing this feature it was determined that most (if not all of the votes) came from an outside source. In an effort to validate this we polled a number of shared hosting providers that we work closely with and this was not a feature they wanted.

    We traced most of the votes down to this single source:

    https://blog.phalconphp.com/post/help-the-community-to-make-phalcon-available-on-cpanel

    While cPanel wants feedback from the general community, our focus is to deliver new features that our Partners and customers are asking for.

    We always appreciate community support and will keep an eye on this feature. Decisions for new features are made by both reviewing this feature request system and talking with Partners and customers. To date out of about 15 conversations, not a single Partner or customer told us this was something that was important to them and thus we adjusted the votes to what we felt was more in line with the community using our system.

    I have a couple of issues with the above reply.

    • For starters there was no communication and no warning that would have indicated what was going to happen with our votes. cPanel decided on this on their own. They do have the right to do so, it's their software after all, but a little courtesy would have gone a long way.
    • The definition of "customers" is different to me than it is to cPanel. As customers cPanel defines their partners and hosting companies, the ones that purchase their software. Myself on the other hand considers customers also the end user, me and you who visit a hosting company and purchase services from them. If we do not exist, then a hosting company and subsequently cPanel does not exist either.
    • cPanel asked a number of their partners who have not heard of Phalcon before, and as such they acted based on that premise. What would have happened if their sample was a different one? Say they asked partner X instead of partner Y? The bias of statistics is based on the sample one chooses and at times (such as this one) it could lead to false results.

    In my reply I pointed out the above, stating that a customer should effectively be the end user and if not, at least their voice should be heard. The reply that I got was as follows:

    Thanks for the reply and understanding. In reviewing the votes that came in, most appeared to be brand advocates of Phalcon and it was very difficult to discern the legitimacy of the votes. Had the votes come from active users of forums.cpanel.net (where most of the users originate from) or we were able to relate them back to some sort of hosting entity, we would have left the votes.

    From the outside looking in the votes just appeared to come from Phalcon users without ties to cPanel & WHM. What I would encourage you to do is the following:

    • Have your users ask their hosting provider about it.
    • Remove the blog links and tweets and allow the feature to grow it's own set of wheels.

    If this feature is truly in high demand from customers of cPanel, Inc. they will naturally vote for it.

    We do appreciate your support and ongoing efforts to get this in front of us. The massive amounts of votes and comments it received, put it on our developers radars and we will continue to monitor the situation.

    So in essence, if we want to achieve our objective, i.e. get Phalcon as an available extension for cPanel, we need to advise the community (but be careful to Remove the blog links and tweets and allow the feature to grow it's own set of wheels.) to contact their hosting companies (that use cPanel) to in turn contact cPanel and request Phalcon to be included as an extension.

    The fallacy of the above is that developers of Phalcon will not choose hosts that offer cPanel because they cannot install the application. If they cannot install the application they will not use hosts that offer cPanel, thus they cannot ask their hosts to include Phalcon as a cPanel extension, and so goes the chicken and egg situation.

    I totally respect cPanel's decisions - I don't agree with them but I do respect them. It's their house their rules as they say. I am however saddened by the fact that we never got any communication or warning that our votes were removed. A bit of communication there would have definitely saved a lot of frustration at least for our community.

    Concluding, if anyone has a cPanel hosting account and wants to see Phalcon available as an extension, feel free to contact your host and request it to be included as an available extension.

    2013-07-25 Update: A great analogy and reply has been posted by Andres in the Phalcon Forum.

  • 2013-05-27 12:00:00

    2013 Memorial Day

    Once again we celebrate Memorial Day in the USA.

    A very special day, dedicated to remembering and honoring all those men and women in uniform that fought for freedom. Freedom that nowadays - in most cases - is taken for granted.

    A personal thank you to all of those that paid the ultimate price, with their life, so that my family, myself, my neighbors, my co-patriots can enjoy what we have today: our freedom.

    You might also enjoy material from the following resources:

    My posts of 2010 and 2012 as well as http://www.usamemorialday.org.