Automate PHP with Phake – Real World Examples

Tweet
This entry is part 2 of 2 in the series Automating PHP with Phake

Automating PHP with Phake

In part one, we covered the basics of Phake and demonstrated ways of executing tasks with it, covering groups, dependencies, and arguments. In this part, we’ll look at some sample real world applications of Phake. Note that the following examples are largely based on things that I usually do manually that need some sort of automation.

Uploading Files to Server with a Phake task

Let’s start by writing a task that will upload files to an FTP server.

task('up', function($args){

  $host = $args['host'];
  $user = $args['user'];
  $pass = $args['pass'];
  $port = 21; //default ftp port
  $timeout = 60; //timeout for uploading individual files
  
  //connect to server
  $ftp = ftp_connect($host, $port, $timeout);
  ftp_login($ftp, $user, $pass); //login to server

  $root_local_dir = $args['local_path'];
  $root_ftp_dir = $args['remote_path'];
  $dir_to_upload = $args['local_path'];

  $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir_to_upload), RecursiveIteratorIterator::SELF_FIRST);

  //iterate through all the files and folders in the specified directory
  foreach($iterator as $path){
    //if the current path refers to a file
    if($path->isFile()){	

      $current_file = $path->__toString();       	
      $dest_file = str_replace($root_local_dir, $root_ftp_dir, $current_file);   	
      $result = ftp_nb_put($ftp, $dest_file, $current_file, FTP_BINARY);
      
      //while the end of the file is not yet reached keep uploading
      while($result == FTP_MOREDATA){
        $result = ftp_nb_continue($ftp);
      }
      
      //once the whole file has finished uploading
      if($result == FTP_FINISHED){
        echo "uploaded: " . $current_file . "\n";
      }
			
     }else{
       //if the current path refers to a directory
       $current_dir = $path->__toString();
   
       //if the name of the directory doesn't begin with dot
       if(substr($current_dir, -1) != '.'){ 
         //remove the path to the directory from the current path
         $current_dir = str_replace($root_local_dir, '', $current_dir);
         //remove the beginning slash from current path
         $current_dir = substr($current_dir, 1);
          
         //create the directory in the server
         ftp_mksubdirs($ftp, $root_ftp_dir, $current_dir);
         echo "created dir: " . $current_dir . "\n";
       }
     } 
   }
});

As you can see from the code above, the task accepts five arguments:

  • host – the domain name or the ip address of the server where you want to upload the files
  • user
  • pass
  • local_path – the path that you want to upload
  • remote_path – the path in the server

We then pass on these arguments to the ftp_connect method. The ftp_connect method returns an ftp stream which we then supply as a first argument for the ftp_login method along with the username and password.

After that, we can iterate through all the files in the local path using the recursive iterator iterator and recursive directory iterator. The recursive iterator iterator takes up a recursive directory iterator object as its first argument and the second argument is a constant from the RecursiveIteratorIterator class. In our task, we have supplied RecursiveIteratorIterator::SELF_FIRST as the second argument. This instructs the iterator to include the full path in every iteration. We then loop through all the files and directories in that path using a foreach loop.

We can check whether the current path is a file using the isFile method. If it’s a file, we just convert it to a string using the __toString magic method, and once converted we assign it to the $current_file variable. Next, we declare another variable ($dest_file) that will hold the path to the current file without the path to the root directory. This will be used as the name of the path for the file that will be created on the server. Once those variables have values assigned to them, we can supply them as the arguments for the ftp_nb_put method. This takes the ftp stream which was returned by the ftp_connect method earlier as the first argument. The second argument is the path to the file on the server and the third one is the path to the file in your computer. The fourth argument is the mode of transfer; in this case we used FTP_BINARY. We can then upload the file by using the ftp_nb_continue method which takes the ftp stream as its argument. As long as this method returns the FTP_MOREDATA constant it means the end of the file still hasn’t been reached, so we’re using a while loop to check for it. Once it’s done it returns FTP_FINISHED.

If the current path refers to a directory, we also convert it to a string, then check if it begins with ., because as you might already know we need to exclude directories that begin with .. A popular example is the .git directory that git uses to store history. We need to exclude these directories as they’re often not needed for the app or website to run and contain meta data or versioning info.

If the current directory isn’t a hidden directory, we remove the root path from it because we don’t need to create that path in the server. That is why we’re using str_replace to replace the part that we don’t need with an empty string.

Once that’s done we can just call the ftp_mksubdirs method to create the path in the server.

The ftp_mksubdirs method takes the ftp stream, the root directory in the server and the path to the current directory. Note that this is not a method that’s built into PHP; we first need to declare it before we can use it. Here’s the function:

function ftp_mksubdirs($ftpcon, $ftpbasedir, $ftpath){
   @ftp_chdir($ftpcon, $ftpbasedir);
   $parts = explode('/',$ftpath); 
   foreach($parts as $part){
      if(!@ftp_chdir($ftpcon, $part)){
	ftp_mkdir($ftpcon, $part);
	ftp_chdir($ftpcon, $part);
      }
   }
}

What this function does is create the directories if they do not exist yet. So if you pass in the following path:

app/views/users

…and the the app, views and users directories don’t exist on the server yet, it creates all of them.

You can run the task above by executing a command like this one in the terminal:

phake up host=someserver.com user=me pass=secret local_path=/home/wern/www/someproject remote_path=/public_html

Note that this task applies to my own personal use case mostly – you might be doing your deploys another way, or you might not be needing the str_replace part at all. What matters is that this is a live task that I actually use, and which makes my life easier.

Seeding the Database

With Phake we can also write a task that will seed the database for us. For that, we’re going to need to use a library called Faker. To use Faker include the following in your project’s composer.json file:

{
  "require": {
    "fzaninotto/faker": "1.5.*@dev" 
  }
}

Next create a task and call it seed_users. It will take one argument called rows. This allows you to set the number of rows you want to insert into your tables.

task('seed_users', function($args){
    $rows = 1000;
    if(!empty($args['rows']){
        $count = $args['rows'];
    }
});

Next, we connect to the database using Mysqli:

$host = 'localhost';
$user = 'user';
$pass = 'secret';
$database = 'test';

$db = new Mysqli($host, $user, $pass, $database);

Then, we initialize a new Faker generator and assign it to the $faker variable. With a for loop, we repeatedly execute the code which generates and saves the user data into the database. We set it to execute until it reaches the number of rows that we have as an argument. By default it will execute 100 times which generates 100 random users, but all with the same password, so we can easily log in as one in case we need to.

$faker = Faker\Factory::create();

for($x = 0; $x <= $rows; $x++){

    $email = $faker->email;
    $password = password_hash('secret', PASSWORD_DEFAULT);
    $first_name = $faker->firstName;
    $last_name = $faker->lastName;
    $address = $faker->address;

    $db->query("INSERT INTO users SET email = '$email', password = '$password'");

    $db->query("INSERT INTO user_info SET first_name = '$first_name', last_name = '$last_name', address = '$address'");
}

echo "Database was seeded!\n";

Putting everything together we end up with the following task:

task('seed_users', function($args){
        
  $rows = 1000;
  if(!empty($args['rows']){
    $rows = $args['rows'];
  }

  $host = 'localhost';
  $user = 'user';
  $pass = 'secret';
  $database = 'test';

  $db = new Mysqli($host, $user, $pass, $database); 
  $faker = Faker\Factory::create();

  for($x = 0; $x <= $rows; $x++){

    $email = $faker->email;
    $password = password_hash('secret', PASSWORD_DEFAULT);
    $first_name = $faker->firstName;
    $last_name = $faker->lastName;
    $address = $faker->address;

    $db->query("INSERT INTO users SET email = '$email', password = '$password'");

    $db->query("INSERT INTO user_info SET first_name = '$first_name', last_name = '$last_name', address = '$address'");
  }

  echo "Database was seeded!\n";
});

Once that’s done you can call the task by executing the following command:

phake seed_users rows=1000

Syncing Data

Another task that I usually find myself doing repetitively is syncing the database changes from my local machine to a server. With Phake, this can be easily automated by using mysqldump, a database backup program that comes with MySQL. From the task, all you have to do is execute the mysqldump command using the exec method in PHP. The mysqldump program accepts the name of the database, the user and the password as its arguments. You can then pipe it to the ssh command. Once ssh is executed, it will ask you for the password, and once you’ve entered the password it will execute the mysql $remote_db command. What this does is get the query that was dumped by the mysqldump command earlier and execute it on the server. Note that this overwrites whatever it is that’s currently on the server so this is only recommended when you’re still in the development phase of your app.

task('sync_db', function($args){
  $db = $args['db']; //name of the local database
  $user = $args['db_user']; //database user
  $pass = $args['db_pass']; //the user's password
  $host = $args['host']; //domain name of the server you want to connect to
  $ssh_user = $args['ssh_user']; //the user used for logging in
  $remote_db = $args['remote_db']; //the name of the database in the server
  exec("mysqldump $db -u $user -p$pass | ssh $ssh_user@$host mysql $remote_db");
});

You can call the task above by using the following command. Just substitute the credentials for your own:

phake sync_db db=my_db db_user=me db_pass=secret ssh_user=me host=somewebsite.com remote_db=my_db

Note that if this doesn’t work you might need to set the following values in your sshd_config file located at /etc/ssh/sshd_config:

X11Forwarding yes
X11UseLocalhost no

Once that’s done reload sshd for the changes to take effect:

sudo /etc/init.d/sshd reload

Conclusion

Phake is a nice way to automate mundane tasks and save time by making the computer do the work for you. With Phake, you can automate pretty much anything that you can think of. It provides you with with all the necessary tools for task automation. If you find yourself doing something repetitive, think of how you can automate it with Phake.

Got any interesting tasks and use cases you’d like to share with us? Let us know in the comments.

Automating PHP with Phake

<< Automate PHP with Phake – Introduction

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Steve Robillard

    Does phake have a plugin ecosystem like grunt and gulp have, or a system of common tasks like phing does? I think this is one of the most compelling reasons to select grunt and the like. The ability to write these tasks in the same language as the code is outweighed by the fact that I need to write and update every task myself.

    • Tatsh

      I cannot see a compelling reason to use this either vs all the other ways to write ‘tasks': Makefile (especially if the majority of your tasks are just commands like rsync, mysql, mysqldump, etc), Rakefile, for remote something like Ansible or Fabric. And for PHP you could use Symfony 2 Console component (which a lot of libraries already have tasks for).

      • gggeek

        Well, makefiles are not very portable.
        If you are writing scripts for an off-the-shelf software, you are seriously limiting its reach.

        As for rake: the advantage of using the same language for build/deploy scripts as for the main app is that you are lowering the barrier to entry for contributors.

        So I’d recommend using this tool for a community project, not necessarily for an app for a single customer.
        I’d not recommend it for very simple tasks either, but in my experience all deploy scripts become full of complex logic after some time.

        It is true that nowadays you can write most of your complex tasks as composer scripts. But afaik it is neither possible to run any of them individually, nor define dependencies.

        To me the main advantages of using pake have been so far:
        1. library of built-in functions for common tasks such as operating on file-sets and string replacement (a la phing but without the xml)
        2. task dependencies
        3. multiple tasks in one script (as opposed to single sf console commands)

    • http://www.bitfalls.com/ Bruno Skvorc

      No ecosystem for now, good point

  • Jose Manuel Orts

    Many thanks, I don’t know Phake and I think it is a good tool. I’m going to taste it.

    Kind regards!

    BTW:
    In a cool site like this (which I follow from time ago), I’m trying to figure out why you show queries so touchy to fault, like those:

    $db->query(“INSERT INTO users SET email = ‘$email’, password = ‘$password'”);
    $db->query(“INSERT INTO user_info SET first_name = ‘$first_name’, last_name = ‘$last_name’, address = ‘$address'”);

    You must quote those parametres!! ($db->real_escape_string()) I think good programming practices have to be spread from sites as good as this, and never go back, even in simple examples, since it is how we have to work always.

    • http://www.bitfalls.com/ Bruno Skvorc

      Well, regarding good coding practices, I wouldn’t say using MySQLi instead of PDO is a good practice in general. I’d never use “real_escape_string” in real life in any project – only PDO with prepared statements. This was just an example, and when writing your own tasks there’s really no chance of SQL injection anyway.

      • Jose Manuel Orts

        Me neither…(since PDO exists) I agree with you Bruno. That was the first thing I noticed.

        I just was following his example with the use of MySQLi, where the proper would be that. And as example, there is not much more lines to write to show it with PDO!!

        Quickly:

        //the example
        $db = new Mysqli($host, $user, $pass, $database);
        $db->query(“INSERT INTO users SET email = ‘$email’, password = ‘$password'”);

        //PDO
        $db = new PDO($dsn, $user, $pass);
        $b = $db->prepare(‘INSERT INTO users SET email = :email, password = :password’);
        $db->execute(array(‘:email’ => $email, ‘:password’ => $password));

        Only ine line more per query…

        I don’t find any reasons to don’t show the example like the last one. Both are just examples. One really good,.. and another one not as good.

        I think this is a cool and visited site where really good techniques and good practices are exposed to PHP developers, some with experience, others without this experience. So, I think we have to write our code even in the most absurd example with all the learned in the proper way. If not, that it serves?

        Kind regards.

        • http://www.bitfalls.com/ Bruno Skvorc

          That’s very true, you’re right. I’ll be more vigilant from now on.

  • Chris Emerson

    I don’t really understand what this is for. If you have to write all the tasks manually yourself, what exactly is the framework doing? Why not use Phing instead which comes with a whole load of built in tasks and the option of extending it with your own if you are doing something uncommon?