• Preface

    I have been using git for quite a while now and am very comfortable with the bread and butter commands such as git clone, git commit. git pull, etc. I love the command line so using some aliases cuts down on typing and speeds up my commit workflow. My motto is commit often and commit small :)

    Git Submodules

    One of the things that I had really no idea about was the git submodules. I knew of the use of them but never ventured into the maze that git submodules can be. According to the git bible:

    Submodules allow you to keep a Git repository as a subdirectory of another Git repository. This lets you clone another repository into your project and keep your commits separate.

    The project

    So lately I have been toying with the idea to introduce the Zephir documentation repository as a submodule to the main repository. This would allow me to pull the code from the main repo easily. The reason for the two repositories if anyone is curious, is that we keep all the versions (as branches) in the zephir-docs repository and also have an intergration with the excellent translation platform Crowdin. They handle all the translations to various languages that Phalcon/Zephir contributors submit, and after that they issue pull requests throught their integration, to our Github repository. This way we get the translated documents often and then need to just update our website.

    In an effort to reduce maintenance and allow for a much faster deployment method, I have been experimenting with Jekyll and Github Pages. To achieve what I wanted I had to have a main site which would serve all the content and also have one submodule per version from the zephir-docs repository.

    Messing up

    I added the submodule using

    $ mkdir 0.11
    $ cd 0.11
    $ git submodule add -b 0.11 [email protected]:niden/zephir-docs.git .
    

    Sadly (for me) I fat fingered the command and now I am left with an unusable submodule.

    Solution

    So how about removing that. It’s not as easy as one might think. With some experimentation and the use of DuckDuckGo I managed to figure out the steps needed to get rid of the unwanted submodule. Note 0.11 is where my submodule lives.

    .gitmodules

    You will notice that a .gitmodules file is present in the root of your folder. Open it with your favorite editor (cough cough nano not vim) and you will see a section similar to this one:

    [submodule "0.11"]
        path = 0.11
        url = git://github.com/niden/zephir-docs.git
    

    Remove that section. If you have other submodules that you need to remove, remove those sections also. Save the file and exit the editor.

    Stage the file
    git add .gitmodules
    

    This is important since the commands below will start issuing warnings if you do not.

    .git/config

    Open .git/config with your favorite editor and you will see a section similar to this one:

    [submodule "0.11"]
        url = git://github.com/niden/zephir-docs.git
    

    Remove it, save the file and exit the editor.

    git rm

    Type the following command in your terminal (at the root of your project):

    $ git rm --cached 0.11 
    

    Ensure that there is no trailing slash. Also if you haven’t staged the .gitmodules file from the step above, it will complain again.

    rm -fR

    Remove the files from the local git reposityro

    $ rm -rR .git/modules/0.11
    
    commit

    Commit the change (remember the staged .gitmodules):

    $ git commit -m "Removed unwanted submodule"
    

    Now you can remove the files of the submodule from your file system.

    $ rm -fR 0.11/
    

    Enjoy!

  • 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

  • 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 [GPR:11530][GI:11486] 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 [GI:12008][GPR: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 in Mvc\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 [GI:11458]

    • 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 [GI:11448]

    $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">&lt;script&gt;alert(&#039;This will execute as JavaScript!&#039;)&lt;/script&gt;</div>
    

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

    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 [GI:2573][GPR: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 [GI:11286]

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

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

    Phalcon\Mvc\Model\Transaction\Manager now correctly keeps account of transactions [GI:11554]

    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 [GI:12004][GPR: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 [GI:2573]

    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 CRYPT_STD_DES, CRYPT_EXT_DES, MD5, CRYPT_SHA256

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

    • Added missing unit-tests for Phalcon\Security

    SESSION

    • Removed Phalcon\Session [GI:11340]

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

    • Fixed the Session write callback [GI:11733]

    TEXT

    • Added ability to use custom delimiter for Phalcon\Text::camelize and Phalcon\Text::uncamelize [GI:10396]

    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 [GI:11215]

    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 [GI:1933] 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 [GI:11386]

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

    Phalcon\Validation\Validator\Digit now correctly validates digits [GI:11374]

    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 [GI:10405]

    • Added convert option to the Phalcon\Validation\Validator\Uniqueness to convert values to the database lookup [GI:12005][GPR: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 [GI:11410][GPR:11441]

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

    DOCUMENTATION

    • Added Indonesian translation [GPR:840]

    VARIOUS

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

    • Fixed Filter::add method handler [GI:11581]

    • Fixed issue with radio not being checked when default value is 0 [GI:11358]

    • 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 [GI:11725]

    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

  • 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
    

    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

  • 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!