Tuesday, August 16, 2016

How to Start a Telegram Bot With PHP

If you are reading this, you know that chat bots are one of the biggest tech trends of 2016.

The bot revolution is not only about artificial intelligence. A bot can be a tool in your messenger with a simple chat interface that can be used to extend the functionality of sites or services or can even be a standalone application. Bots are cheaper to develop and easier to install, and another great feature is that messengers can be used on every type of device—laptops, smartphones, and tablets. That's why everybody is crazy about bots now.

And the biggest messenger with an open bot API is Telegram.

What We Are Going to Do

In this article we will create a simple stopwatch Telegram bot. I will show you how to create your bot, connect with analytics, write some code, and finally add your bot to a bot store.

By the way, I've already prepared a demo, so you can test it just by adding @stopwatchbot to your Telegram contact list.

Create a Bot With BotFather

The first step to creating a bot is to register the account of your bot in Telegram. And there is a bot for that, called the BotFather. Just add it to your contact list and you'll be able to create and set up Telegram bots, just by typing the /newbot command and following the instructions of BotFather.


After registering your new bot, you will receive a congratulations message with an authorization token. We will use this token soon to authorize a bot and send requests to the Bot API.

Later you can use BotFather to add descriptions or photos to the profiles of your bots, regenerate tokens, set lists of commands to use, delete accounts, and so on. To get a full list of commands, just type /help in a chat to get a list of BotFather's commands.

Connect to Botan Analytics

There is no built-in analytics in the Telegram Bots API, but it's important to know how many users you have, how they act, and which commands they trigger more. Of course, we can collect this information using our own engine, but if we want to focus on bot functionality, not metrics, we just need to use an out-of-the-box solution.

And there is a simple tool to connect your bot to analytics, called Botan. It's based on Yandex AppMetric and completely free. Using Botan, you can segment your audience, get information about user profiles, get the most used command, and get beautiful graphs right in your messenger, like this:


To get started, you need to register your bot in Botan and get a token. And again, you can do it with a bot, BotanioBot:


Just click the “Add bot” key on the dialog keyboard, type the nick of your bot, and you will get your bot track token. Now Botanio is ready to track your bot events, and you can get statistics by users, sessions, retention and events right in your messenger.

Create and Register an SSL Webhook

In Telegram there are two ways to get messages from your users: long polling and webhooks.


Basically, with long polling, you need to request new messages from the API, and with webhooks you are setting a callback that the Telegram API will call if a new message arrives from a user. I prefer to use webhooks because it looks like real-time communication, so in this article we will use this method too. Now we need to choose a callback URL for our webhook, which needs to be reached under the HTTPS protocol, and we need to set it really secure, so hide your script in a secret path, as the manual says:
If you'd like to make sure that the Webhook request comes from Telegram, we recommend using a secret path in the URL, e.g. https://www.example.com/<token>. Since nobody else knows your bot‘s token, you can be pretty sure it’s us.
If your SSL certificate is trusted, all you need to do is open this URL in your browser:
  1. https://api.telegram.org:443/bot[token]/setwebhook?url=[webhook]
Otherwise you have to generate a self-signed certificate. Here is an example of the command on Linux for it:
  1. openssl req -newkey rsa:2048 -sha256 -nodes -keyout /path/to/certificate.key -x509 -days 365  -out /path/to/certificate.crt -subj "/C=IT/ST=state/L=location/O=description/CN=yourdomain.com"
And don't forget to open the SSL port:
  1. sudo ufw allow 443/tcp
To get the certificate checked and set your webhook domain to trusted, you need to upload your public key certificate:
  1. curl \
  2.   -F "url=https://yourdomain.com/path/to/script.php" \
  3.   -F "certificate=/path/to/certificate.key" \
  4.   "https://api.telegram.org/bot[token]/setwebhook"
Finally you will get a JSON reply like this:
  1. {"ok":true,"result":true,"description":"Webhook was set"}
It says that the webhook was set and we are ready to start the engine of the bot.

Build a Database

Now we need to build a database for our timers. What do we need to store in it? When a user commands the stopwatch to start, we will take the ID of the chat and save a row with the chat ID and current Unix time, which is the number of seconds between now and the start of Unix Epoch, which is 1 January 1970 at UTC. Consequently, we will save a row with the chat ID and integer timestamp of the current Unix time.

To show the current stopwatch time, we will get the saved timestamp and compare it with the current timestamp. The difference will be the current time in seconds. If the user stops the timer, we will simply delete the row with the current chat ID.

So let's create a database and table to store the stopwatch information:
  1. CREATE TABLE IF NOT EXISTS `stopwatch` (
  2.   `chat_id` int(10) unsigned NOT NULL,
  3.   `timestamp` int(10) unsigned NOT NULL,
  4.   PRIMARY KEY (`chat_id`)
  5. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Create a Stopwatch Class

Finally we are ready to start coding. Let's create a class to work with the database in a file called stopwatch.php and start with a constructor that will set two private variables, where we will store the chat ID and the current MySQL connection:
  1. class Stopwatch
  2. {
  3.     /** @var mysqli */
  4.     private $mysqli;
  5.     /** @var int */
  6.     private $stopwatch_id;
  7.     /**
  8.      * Stopwatch constructor
  9.      * @param mysqli $mysqli
  10.      * @param $stopwatch_id
  11.      */
  12.     public function __construct(\mysqli $mysqli, $stopwatch_id)
  13.     {
  14.         $this->mysqli = $mysqli;
  15.         $this->stopwatch_id = intval($stopwatch_id);
  16.     }
  17. }
When the user starts the timer, we will get the current Unix time and save it in a row with the chat ID, so here is the start() method:
  1. public function start()
  2. {
  3.     $timestamp = time();
  4.     $query = "
  5.         INSERT INTO  `stopwatch` (`chat_id`, `timestamp`)
  6.         VALUES ('$this->stopwatch_id', '$timestamp')
  7.         ON DUPLICATE KEY UPDATE timestamp = '$timestamp'       
  8.     ";
  9.     return $this->mysqli->query($query);
  10. }
If the timer stops, we need to delete a row from the database:
  1. /**
  2.  * Delete row with stopwatch id
  3.  * @return bool|mysqli_result
  4.  */
  5. public function stop()
  6. {
  7. $query = "
  8.     DELETE FROM `stopwatch`
  9.     WHERE `chat_id` = $this->stopwatch_id
  10.     ";
  11.     return $this->mysqli->query($query);
  12. }
And now for the main part of the class. When the user requests the status of the timer, we need to find the row with the stopwatch from the current conversation and calculate the difference in seconds between the saved Unix time and the current time. Fortunately, Unix time is an integer, so we can just subtract one value from another. To format the resulting value as a time, we will use the gmdate function.
  1. /**
  2.  * Find row with stopwatch id and return difference in seconds from saved Unix time and current time
  3.  * @return string
  4.  */
  5. public function status()
  6. {
  7.     $query = "
  8.         SELECT `timestamp` FROM  `stopwatch`
  9.         WHERE `chat_id` = $this->stopwatch_id        
  10.     ";
  11.     $timestamp = $this->mysqli->query($query)->fetch_row();
  12.     if (!empty($timestamp)) {
  13.         return gmdate("H:i:s", time() - reset($timestamp));
  14.     }
  15. }
As you can see, if there is no value in the database, the method status() will return nothing, and we will process a null value like a stopped timer.

Choosing a PHP Library

There are many PHP libraries that exist to work with the Telegram API, but, at least at the moment of writing this article, there's only one that supports both the Telegram Bot API wrapper and Botan tracking. And it's called PHP Telegram Bot API.

Use Composer to install this library:
  1. composer require telegram-bot/api
If you're not interested in using analytics, try Telegram Bot API PHP SDK with Lavarel integration or PHP Telegram Bot.

Start the Webhook Script

And now the main part begins—we will create a script to process callbacks from the Telegram Bot API. Start a file called index.php and include Composer autoload and a new Stopwatch class. Open a MySQL connection, create a new Telegram API client, and run it:
  1. require_once 'vendor/autoload.php';
  2. require_once 'stopwatch.php';
  3.  
  4. // connect to database
  5. $mysqli = new mysqli('database_host', 'database_user', 'database_password', 'database_name');
  6. if (!empty($mysqli->connect_errno)) {
  7.     throw new \Exception($mysqli->connect_error, $mysqli->connect_errno);
  8. }
  9.  
  10. // create a bot
  11. $bot = new \TelegramBot\Api\Client('bot_token', 'botanio_token');
  12. // run, bot, run!
  13. $bot->run();
Create Commands

Now we need to set up a bot to answer on command /start. This command is used to start all Telegram bots, and users will be shown our welcome message when the first chat begins.
  1. $bot->command('start', function ($message) use ($bot) {
  2.     $answer = 'Howdy! Welcome to the stopwatch. Use bot commands or keyboard to control your time.';
  3.     $bot->sendMessage($message->getChat()->getId(), $answer);
  4. });
Here, in the command() method, we defined a closure for receiving a command. This closure gets the ID of the current chat and sends a welcome message. Also, all registered commands are automatically tracked as the command name.

To start the stopwatch, we will define the /go command:
  1. $bot->command('go', function ($message) use ($bot, $mysqli) {
  2.     $stopwatch = new Stopwatch($mysqli, $message->getChat()->getId());
  3.     $stopwatch->start();
  4.     $bot->sendMessage($message->getChat()->getId(), 'Stopwatch started. Go!');
  5. });
This will create an instance of the Stopwatch class and start a timer calling the start() method that we have defined already.

To define the /status command, we need to do the same thing. Just call the status() method and return the result. If the method returned null, tell the user that the timer is not started.
  1. $bot->command('status', function ($message) use ($bot, $mysqli) {
  2.     $stopwatch = new Stopwatch($mysqli, $message->getChat()->getId());
  3.     $answer = $stopwatch->status();
  4.     if (empty($answer)) {
  5.         $answer = 'Timer is not started.';
  6.     }
  7.     $bot->sendMessage($message->getChat()->getId(), $answer);
  8. });
And if the user stops the timer, we need to get the status first, show the resulting time, and stop the timer using the stop() method.
  1. $bot->command('stop', function ($message) use ($bot, $mysqli) {
  2.     $stopwatch = new Stopwatch($mysqli, $message->getChat()->getId());
  3.     $answer = $stopwatch->status();
  4.     if (!empty($answer)) {
  5.         $answer = 'Your time is ' . $answer . PHP_EOL;
  6.     }
  7.     $stopwatch->stop();
  8.     $bot->sendMessage($message->getChat()->getId(), $answer . 'Stopwatch stopped. Enjoy your time!');
  9. });
That's it! Now you can upload all the files to the webhook directory and test your bot.

Adding a Keyboard

To suggest to the user which commands he or she can run, we can add a keyboard to a message. Our stopwatch can be running or stopped, and there will be two ones for each state. To show a keyboard to the user, we just need to extend the sendMessage() method:
  1. $keyboard = new \TelegramBot\Api\Types\ReplyKeyboardMarkup([['/go', '/status']], null, true);
  2.  
  3. $bot->sendMessage($message->getChat()->getId(), $answer, false, null, null, $keyboards);
  4. });
Now you can add keyboards to every command of your bot. I will not include a full example here, but you can see it in the repository pages.

Adding Your Bot to a Store

Okay, so now we have working bot, and we want to show it to the world. The best way is to register the bot in a bot catalogue. For now, Telegram doesn't have an official catalogue like this, but there are a few unofficial ones, and the biggest is Storebot.me, where thousands of bots are already registered.

And there is a... bot to register your bot in a bot store! Add @storebot to your contact list, type the /add command, and follow the instructions. You will be asked to enter the bot's username, name, and description, choose one of the standard categories, and confirm the bot's ownership by sending its token.


After a while, your bot will pass the submission process and appear in the Storebot charts. Now you and your users can vote, find and rate your bot in the bot store to help it rise to the top of the chart.

Conclusion

We've come a long way, from creating a baby bot to registering it in a store to be available to real users. As you can see, there are many tools that exist to make your life easier with creating and spreading your bot, and you don't need much code to start an easy bot. Now you are ready to make your own!
Written by Anton Bagaiev

If you found this post interesting, follow and support us.
Suggest for you:

The Complete PHP 7 Guide for Web Developers

Up to Speed with PHP 7

Learn PHP 7 This Way to Rise Above & Beyond Competion!

PHP MySQL Database Connections

The Complete PHP with MySQL Developer Course (New)

Monday, August 15, 2016

Best Practices for Modern PHP Development_part 2 (end)


4 Object calisthenics

This isn't a full dive into these principles, but the first two are easy to remember, provide good value, and can be immediately applied to just about any codebase.

4.1 No more than one level of indentation per method

This is a helpful way to think about decomposing methods into smaller chunks, leaving you with code that's clearer and more self-documenting. The more levels of indentation you have, the more the method is doing and the more state you have to keep track of in your head while you're working with it.

Right away I know people will object to this, but this is just a guideline/heuristic, not a hard and fast rule. I'm not expecting anyone to enforce PHP_CodeSniffer rules for this (although people have).

Let's run through a quick sample of what this might look like:
  1. public function transformToCsv($data)
  2. {
  3.     $csvLines = array();
  4.     $csvLines[] = implode(',', array_keys($data[0]));
  5.     foreach ($data as $row) {
  6.         if (!$row) {
  7.             continue;
  8.         }
  9.         $csvLines[] = implode(',', $row);
  10.     }

  11.     return $csvLines;
  12. }
While this isn't terrible code (it's technically correct, testable, etc...) we can do a lot more to make this clear. How would we reduce the levels of nesting here?

We know we need to vastly simplify the contents of the foreach loop (or remove it entirely) so let's start there.
  1. if (!$row) {
  2.     continue;
  3. }
This first bit is easy. All this is doing is ignoring empty rows. We can shortcut this entire process by using a built-in PHP function before we even get to the loop.
  1. $data = array_filter($data);
  2. foreach ($data as $row) {
  3.     $csvLines[] = implode(',', $row);
  4. }
We now have our single level of nesting. But looking at this, all we are doing is applying a function to each item in an array. We don't even need the foreach loop to do that.
  1. $data = array_filter($data);
  2. $csvLines =+ array_map('implode', $data, array_fill(0, count($data), ',');
Now we have no nesting at all, and the code will likely be faster since we're doing all the looping with native C functions instead of PHP. We do have to engage in a bit of trickery to pass the comma to implode though, so you could make the argument that stopping at the previous step is much more understandable.

4.2 Try not to use else

This really deals with two main ideas. The first one is multiple return statements from a method. If you have enough information do make a decision about the method's result, go ahead make that decision and return. The second is an idea known as Guard Clauses. These are basically validation checks combined with early returns, usually near the top of a method. Let me show you what I mean.
  1. public function addThreeInts($first, $second, $third) {
  2.     if (is_int($first)) {
  3.         if (is_int($second)) {
  4.             if (is_int($third)) {
  5.                 $sum = $first + $second + $third;
  6.             } else {
  7.                 return null;
  8.             }
  9.         } else {
  10.             return null;
  11.         }
  12.     } else {
  13.         return null;
  14.     }

  15.     return $sum;
  16. }
So this is pretty straightforward again, it adds 3 ints together and returns the result, or null if any of the parameters are not an integer. Ignoring the fact that we could combine all those checks onto a single line with AND operators, I think you can see how the nested if/else structure makes the code harder to follow. Now look at this example instead.
  1. public function addThreeInts($first, $second, $third) {
  2.     if (!is_int($first)) {
  3.         return null;
  4.     }

  5.     if (!is_int($second)) {
  6.         return null;
  7.     }

  8.     if (!is_int($third)) {
  9.         return null;
  10.     }

  11.     return $first + $second + $third;
  12. }
To me this example is much easier to follow. Here we're using guard clauses to verify our initial assertions about the parameters we're passing and immediately exiting the method if they don't pass. We also no longer have the intermediate variable to track the sum all the way through the method. In this case we've verified that we're already on the happy path and we can just do what we came here to do. Again we could just do all those checks in one if but the principle should be clear.

5 Unit testing

Unit testing is the practice of writing small tests that verify behavior in your code. They are almost always written in the same language as the code (in this case PHP) and are intended to be fast enough to run at any time. They are extremely valuable as a tool to improve your code. Other than the obvious benefits of ensuring that your code is doing what you think it is, unit testing can provide very useful design feedback as well. If a piece of code is difficult to test, it often showcases design problems. They also give you a safety net against regressions, and that allows you to refactor much more often and evolve your code to a cleaner design.

5.1 Tools

There are several unit testing tools out there in PHP, but far and away the most common is PHPUnit. You can install it by downloading a PHAR file directly, or install it with composer. Since we are using composer for everything else, we'll show that method. Also, since PHPUnit is not likely going to be deployed to production, we can install it as a dev dependency with the following command:
  1. composer require --dev phpunit/phpunit
5.2 Tests are a specification

The most important role of unit tests in your code is to provide an executable specification of what the code is supposed to do. Even if the test code is wrong, or the code has bugs, the knowledge of what the system is supposed to do is priceless.

5.3 Write your tests first

If you've had the chance to see a set of tests written before the code and one written after the code was finished, they're strikingly different. The "after" tests are much more concerned with the implementation details of the class and making sure they have good line coverage, whereas the "before" tests are more about verifying the desired external behavior. That's really what we care about with unit tests anyway, is making sure the class exhibits the right behavior. Implementation-focused tests actually make refactoring harder because they break if the internals of the classes change, and you've just cost yourself the information hiding benefits of OOP.

5.4 What makes a good unit test

Good unit tests share a lot of the following characteristics:
  • Fast - should run in milliseconds.
  • No network access - should be able to turn off wireless/unplug and all the tests still pass.
  • Limited file system access - this adds to speed and flexibility if deploying code to other environments.
  • No database access - avoids costly setup and teardown activities.
  • Test only one thing at a time - a unit test should have only one reason to fail.
  • Well-named - see 5.2 above.
  • Mostly fake objects - the only "real" objects in unit tests should be the object we're testing and simple value objects. The rest should be some form of test double
There are reasons to go against some of these but as general guidelines they will serve you well.

5.5 When testing is painful
Unit testing forces you to feel the pain of bad design up front - Michael Feathers
When you're writing unit tests, you're forcing yourself to actually use the class to accomplish things. If you write tests at the end, or worse yet, just chuck the code over the wall for QA or whoever to write tests, you don't get any feedback about how the class actually behaves. If we're writing tests, and the class is a real pain to use, we'll find out while we're writing it, which is nearly the cheapest time to fix it.

If a class is hard to test, it's a design flaw. Different flaws manifest themselves in different ways, though. If you have to do a ton of mocking, your class probably has too many dependencies, or your methods are doing too much. The more setup you have to do for each test, the more likely it is that your methods are doing too much. If you have to write really convoluted test scenarios in order to exercise behavior, the class's methods are probably doing too much. If you have to dig inside a bunch of private methods and state to test things, maybe there's another class trying to get out. Unit testing is very good at exposing "iceberg classes" where 80% of what the class does is hidden away in protected or private code. I used to be a big fan of making as much as possible protected, but now I realized I was just making my individual classes responsible for too much, and the real solution was to break the class up into smaller pieces.
Written by Brian Fenton

If you found this post interesting, follow and support us.
Suggest for you:

The Complete PHP with MySQL Developer Course (New)

Learn what's new in PHP 7

Modern Programming with PHP

PHP and JWT Tutorial Make a Two-Factor Authentication System

How to Insert Data into MySQL Database with PHP


Saturday, August 13, 2016

Best Practices for Modern PHP Development_part 1


1 Setup and configuration

1.1 Keep Current

Let's call this out from the very beginning - a depressingly small number of PHP installs in the wild are current, or kept current. Whether that is due to shared hosting restrictions, defaults that no one thinks to change, or no time/budget for upgrade testing, the humble PHP  binaries tend to get left behind. So one clear best practice that needs more emphasis is to always use a current version of PHP (5.6.x as of this article). Furthermore, it's also important to schedule regular upgrades of both PHP itself and any extensions or vendor libraries you may be using. Upgrades get you new language features, improved speed, lower memory usage, and security updates. The more frequently you upgrade, the less painful the process becomes.

1.2 Set sensible defaults

PHP does a decent job of setting good defaults out of the box with its php.ini.development and php.ini.production files, but we can do better. For one, they don't set a date/timezone for us. That makes sense from a distribution perspective, but without one, PHP will throw an E_WARNING error any time we call a date/time related function. Here are some recommended settings:
  • date.timezone - pick from the list of supported timezones
  • session.save_path - if we're using files for sessions and not some other save handler, set this to something outside of /tmp. Leaving this as /tmp can be risky on a shared hosting environment since /tmp is typically wide open permissions-wise. Even with the sticky-bit set, anyone with access to list the contents of this directory can learn all of your active session IDs.
  • session.cookie_secure - no brainer, turn this on if you are serving your PHP code over HTTPS.
  • session.cookie_httponly - set this to prevent PHP session cookies from being accessible via JavaScript
  • More... use a tool like iniscan to test your configuration for common vulnerabilities
1.3 Extensions

It's also a good idea to disable (or at least not enable) extensions that you won't use, like database drivers. To see what's enabled, run the phpinfo() command or go to a command line and run this.
  1. $ php -i
The information is the same, but phpinfo() has HTML formatting added. The CLI version is easier to pipe to grep to find specific information though. Ex.
  1. $ php -i | grep error_log
One caveat of this method though: it's possible to have different PHP settings apply to the web-facing version and the CLI version.

2 Use Composer

This may come as a surprise but one of the best practices for writing modern PHP is to write less of it. While it is true that one of the best ways to get good at programming is to do it, there are a large number of problems that have already been solved in the PHP space, like routing, basic input validation libraries, unit conversion, database abstraction layers, etc... Just go to Packagist and browse around. You'll likely find that significant portions of the problem you're trying to solve have already been written and tested.

While it's tempting to write all the code yourself (and there's nothing wrong with writing your own framework or library as a learning experience) you should fight against those feelings of Not Invented Here and save yourself a lot of time and headache. Follow the doctrine of PIE instead - Proudly Invented Elsewhere. Also, if you do choose to write your own whatever, don't release it unless it does something significantly different or better than existing offerings.

Composer is a package manager for PHP, similar to pip in Python, gem in Ruby, and npm in Node. It lets you define a JSON file that lists your code's dependencies, and it will attempt to resolve those requirements for you by downloading and installing the necessary code bundles.

2.1 Installing Composer

We're assuming that this is a local project, so let's install an instance of Composer just for the current project. Navigate to your project directory and run this:
  1. $ curl -sS https://getcomposer.org/installer | php
Keep in mind that piping any download directly to a script interpreter (sh, ruby, php, etc...) is a security risk, so do read the install code and ensure you're comfortable with it before running any command like this.

For convenience sake (if you prefer typing composer install over php composer.phar install, you can use this command to install a single copy of composer globally:
  1. $ mv composer.phar /usr/local/bin/composer
  2. $ chmod +x composer
You may need to run those with sudo depending on your file permissions.

2.2 Using Composer

Composer has two main categories of dependencies that it can manage: "require" and "require-dev". Dependencies listed as "require" are installed everywhere, but "require-dev" dependencies are only installed when specifically requested. Usually these are tools for when the code is under active development, such as PHP_CodeSniffer. The line below shows an example of how to install Guzzle, a popular HTTP library.
  1. $ php composer.phar require guzzle/guzzle
To install a tool just for development purposes, add the --dev flag:
  1. $ php composer.phar require --dev 'sebastian/phpcpd'
This installs PHP Copy-Paste Detector, another code quality tool as a development-only dependency.

2.3 Install vs update

When we first run composer install it will install any libraries and their dependencies we need, based on the composer.json file. When that is done, composer creates a lock file, predictably called composer.lock. This file contains a list of the dependencies composer found for us and their exact versions, with hashes. Then any future time we run composer install, it will look in the lock file and install those exact versions.

composer update is a bit of a different beast. It will ignore the composer.lock file (if present) and try to find the most up to date versions of each of the dependencies that still satisfies the constraints in composer.json. It then writes a new composer.lock file when it's finished.

2.4 Autoloading

Both composer install and composer update will generate an autoloader for us that tells PHP where to find all the necessary files to use the libraries we've just installed. To use it, just add this line (usually to a bootstrap file that gets executed on every request):
  1. require 'vendor/autoload.php';

3 Follow good design principles

3.1 SOLID

SOLID is a mnemonic to remind us of five key principles in good object-oriented software design.

3.1.1 S - Single Responsibility Principle

This states that classes should only have one responsibility, or put another way, they should only have a single reason to change. This fits nicely with the Unix philosophy of lots of small tools, doing one thing well. Classes that only do one thing are much easier to test and debug, and they are less likely to surprise you. You don't want a method call to a Validator class updating db records. Here's an example of an SRP violation, the likes of which you'd commonly see in an application based on the ActiveRecord pattern.
  1. class Person extends Model
  2. {
  3.     public $name;
  4.     public $birthDate;
  5.     protected $preferences;

  6.     public function getPreferences() {}

  7.     public function save() {}
  8. }
So this is a pretty basic entity model. One of these things doesn't belong here though. An entity model's only responsibility should be behavior related to the entity it's representing, it shouldn't be responsible for persisting itself.
  1. class Person extends Model
  2. {
  3.     public $name;
  4.     public $birthDate;
  5.     protected $preferences;

  6.     public function getPreferences() {}
  7. }

  8. class DataStore
  9. {
  10.     public function save(Model $model) {}
  11. }
This is better. The Person model is back to only doing one thing, and the save behavior has been moved to a persistence object instead. Note also that I only type hinted on Model, not Person. We'll come back to that when we get to the L and D parts of SOLID.

3.1.2 O - Open Closed Principle

There's an awesome test for this that pretty well sums up what this principle is about: think of a feature to implement, probably the most recent one you worked on or are working on. Can you implement that feature in your existing codebase SOLELY by adding new classes and not changing any existing classes in your system? Your configuration and wiring code gets a bit of a pass, but in most systems this is surprisingly difficult. You have to rely a lot on polymorphic dispatch and most codebases just aren't set up for that. If you're interested in that there's a good Google talk up on YouTube about polymorphism and writing code without Ifs that digs into it further. As a bonus, the talk is given by Miško Hevery, whom many may know as the creator of AngularJs.

3.1.3 L - Liskov Substitution Principle

This principle is named for Barbara Liskov, and is printed below:
"Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program."
That all sounds well and good, but it's more clearly illustrated with an example.
  1. abstract class Shape 
  2. {
  3.     public function getHeight();

  4.     public function setHeight($height);

  5.     public function getLength();

  6.     public function setLength($length);
  7. }
This is going to represent our basic four-sided shape. Nothing fancy here.
  1. class Square extends Shape
  2. {
  3.     protected $size;

  4.     public function getHeight() {
  5.         return $this->size;
  6.     }

  7.     public function setHeight($height) {
  8.         $this->size = $height;
  9.     }

  10.     public function getLength() {
  11.         return $this->size;
  12.     }

  13.     public function setLength($length) {
  14.         $this->size = $length;
  15.     }
  16. }
Here's our first shape, the Square. Pretty straightforward shape, right? You can assume that there's a constructor where we set the dimensions, but you see here from this implementation that the length and height are always going to be the same. Squares are just like that.
  1. class Rectangle extends Shape
  2. {
  3.     protected $height;
  4.     protected $length;

  5.     public function getHeight() {
  6.         return $this->height;
  7.     }

  8.     public function setHeight($height) {
  9.         $this->height = $height;
  10.     }

  11.     public function getLength() {
  12.         return $this->length;
  13.     }

  14.     public function setLength($length) {
  15.         $this->length = $length;
  16.     }
  17. }
So here we have a different shape. Still has the same method signatures, it's still a four sided shape, but what if we start trying to use them in place of one another? Now all of a sudden if we change the height of our Shape, we can no longer assume that the length of our shape will match. We've violated the contract that we had with the user when we gave them our Square shape.

This is a textbook example of a violation of the LSP and we need this type of a principle in place to make the best use of a type system. Even duck typing won't tell us if the underlying behavior is different, and since we can't know that without seeing it break, it's best to make sure that it isn't different in the first place.

3.1.3 I - Interface Segregation Principle

This principle says to favor many small, fine grained interfaces vs. one large one. Interfaces should be based on behavior rather than "it's one of these classes". Think of interfaces that come with PHP. Traversable, Countable, Serializable, things like that. They advertise capabilities that the object possesses, not what it inherits from. So keep your interfaces small. You don't want an interface to have 30 methods on it, 3 is a much better goal.

3.1.4 D - Dependency Inversion Principle

You've probably heard about this in other places that talked about Dependency Injection, but Dependency Inversion and Dependency Injection aren't quite the same thing. Dependency inversion is really just a way of saying that you should depend on abstractions in your system and not on its details. Now what does that mean to you on a day to day basis?
Don't directly use mysqli_query() all over your code, use something like DataStore->query() instead.
The core of this principle is actually about abstractions. It's more about saying "use a database adapter" instead of depending on direct calls to things like mysqli_query. If you're directly using mysqli_query in half your classes then you're tying everything directly to your database. Nothing for or against MySQL here, but if you are using mysqli_query, that type of low level detail should be hidden away in only one place and then that functionality should be exposed via a generic wrapper.

Now I know this is kind of a hackneyed example if you think about it, because the number of times you're going to actually completely change your database engine after your product is in production are very, very low. I picked it because I figured people would be familiar with the idea from their own code. Also, even if you have a database that you know you're sticking with, that abstract wrapper object allows you to fix bugs, change behavior, or implement features that you wish your chosen database had. It also makes unit testing possible where low level calls wouldn't.
Written by Brian Fenton

If you found this post interesting, follow and support us.
Suggest for you:

Learn PHP 7 This Way to Rise Above & Beyond Competion!

PHP MySQL Database Connections

The Complete PHP with MySQL Developer Course (New)

Learn what's new in PHP 7

Modern Programming with PHP

Monday, August 8, 2016

Using PHP CodeSniffer With WordPress: Installing and Using the WordPress Rules_part2 (end)

3. Running PHP CodeSniffer Against WordPress Projects

Assuming you're working out of a directory that includes a WordPress plugin, then you can skip the following step. If, on the other hand, you do not have a copy of a WordPress script, file, theme, or plugin installed in the project directory, go ahead and copy one over to your project directory now.

As mentioned, we'll be testing the Hello Dolly plugin.

To run PHP CodeSniffer with the WordPress rules against the files in the plugin directory, enter the following command in the Terminal:
  1. $ vendor/bin/phpcs --standard=WordPress hello-dolly
This will result in output that should correspond to what you see here:
  1. FILE: /Users/tommcfarlin/Desktop/tutsplus_demo/hello-dolly/hello.php
  2. ----------------------------------------------------------------------
  3. FOUND 14 ERRORS AFFECTING 14 LINES
  4. ----------------------------------------------------------------------
  5.   2 | ERROR | Missing short description in doc comment
  6.   5 | ERROR | There must be exactly one blank line after the file
  7.     |       | comment
  8.   6 | ERROR | Empty line required before block comment
  9.  15 | ERROR | You must use "/**" style comments for a function
  10.     |       | comment
  11.  46 | ERROR | Inline comments must end in full-stops, exclamation
  12.     |       | marks, or question marks
  13.  49 | ERROR | Inline comments must end in full-stops, exclamation
  14.     |       | marks, or question marks
  15.  53 | ERROR | Inline comments must end in full-stops, exclamation
  16.     |       | marks, or question marks
  17.  54 | ERROR | You must use "/**" style comments for a function
  18.     |       | comment
  19.  56 | ERROR | Expected next thing to be an escaping function (see
  20.     |       | Codex for 'Data Validation'), not '"<p
  21.     |       | id='dolly'>$chosen</p>"'
  22.  59 | ERROR | Inline comments must end in full-stops, exclamation
  23.     |       | marks, or question marks
  24.  62 | ERROR | Inline comments must end in full-stops, exclamation
  25.     |       | marks, or question marks
  26.  63 | ERROR | You must use "/**" style comments for a function
  27.     |       | comment
  28.  64 | ERROR | Inline comments must end in full-stops, exclamation
  29.     |       | marks, or question marks
  30.  67 | ERROR | Expected next thing to be an escaping function (see
  31.     |       | Codex for 'Data Validation'), not '"
  32.     |       | '
  33. ----------------------------------------------------------------------
Of course, some of these things may change depending on when you're reading this tutorial.

The errors should be pretty clear as to what needs to be fixed:
  • The first column denotes the line in which the problem exists.
  • The second column determines if there is an error or a warning.
  • The third column explains the problem and what's expected of the code.
Note that although these are errors or warnings, the code will obviously still work. But let's pull this through end-to-end and see what it's like to fix up a plugin, arguably the most popular since it comes with each installation of WordPress, and review the differences in the quality of the code.

4. Refactoring Hello Dolly

Note the plugin, before we begin working on it, includes the following source code:
  1. <?php
  2. /**
  3.  * @package Hello_Dolly
  4.  * @version 1.6
  5.  */
  6. /*
  7. Plugin Name: Hello Dolly
  8. Plugin URI: https://wordpress.org/plugins/hello-dolly/
  9. Description: This is not just a plugin, it symbolizes the hope and enthusiasm of an entire generation summed up in two words sung most famously by Louis Armstrong: Hello, Dolly. When activated you will randomly see a lyric from <cite>Hello, Dolly</cite> in the upper right of your admin screen on every page.
  10. Author: Matt Mullenweg
  11. Version: 1.6
  12. Author URI: http://ma.tt/
  13. */ 
  14. function hello_dolly_get_lyric() {
  15.     /** These are the lyrics to Hello Dolly */
  16.     $lyrics = "Hello, Dolly
  17. Well, hello, Dolly
  18. It's so nice to have you back where you belong
  19. You're lookin' swell, Dolly
  20. I can tell, Dolly
  21. You're still glowin', you're still crowin'
  22. You're still goin' strong
  23. We feel the room swayin'
  24. While the band's playin'
  25. One of your old favourite songs from way back when
  26. So, take her wrap, fellas
  27. Find her an empty lap, fellas
  28. Dolly'll never go away again
  29. Hello, Dolly
  30. Well, hello, Dolly
  31. It's so nice to have you back where you belong
  32. You're lookin' swell, Dolly
  33. I can tell, Dolly
  34. You're still glowin', you're still crowin'
  35. You're still goin' strong
  36. We feel the room swayin'
  37. While the band's playin'
  38. One of your old favourite songs from way back when
  39. Golly, gee, fellas
  40. Find her a vacant knee, fellas
  41. Dolly'll never go away
  42. Dolly'll never go away
  43. Dolly'll never go away again"; 
  44.     // Here we split it into lines
  45.     $lyrics = explode( "\n", $lyrics ); 
  46.     // And then randomly choose a line
  47.     return wptexturize( $lyrics[ mt_rand( 0, count( $lyrics ) - 1 ) ] );
  48. } 
  49. // This just echoes the chosen line, we'll position it later
  50. function hello_dolly() {
  51.     $chosen = hello_dolly_get_lyric();
  52.     echo "<p id='dolly'>$chosen</p>";
  53. } 
  54. // Now we set that function up to execute when the admin_notices action is called
  55. add_action( 'admin_notices', 'hello_dolly' ); 
  56. // We need some CSS to position the paragraph
  57. function dolly_css() {
  58.     // This makes sure that the positioning is also good for right-to-left languages
  59.     $x = is_rtl() ? 'left' : 'right'; 
  60.     echo "
  61.     <style type='text/css'>
  62.     #dolly {
  63.         float: $x;
  64.         padding-$x: 15px;
  65.         padding-top: 5px;       
  66.         margin: 0;
  67.         font-size: 11px;
  68.     }
  69.     </style>
  70.     ";
  71. } 
  72. add_action( 'admin_head', 'dolly_css' ); 
  73. ?>
It should be relatively easy to follow as it uses only a few basic PHP features and Matt's done a good job of commenting the code.

But given the 14 errors that the CodeSniffer found, let's refactor the plugin. Taking into account the errors they presented and what it's expecting to see, let's address each of them.

Once done, the plugin should look like the following:
  1. <?php
  2. /**
  3.  * This is a plugin that symbolizes the hope and enthusiasm of an entire
  4.  * generation summed up in two words sung most famously by Louis Armstrong.
  5.  *
  6.  * @package Hello_Dolly
  7.  * @version 1.6
  8.  *
  9.  * @wordpress-plugin
  10.  * Plugin Name: Hello Dolly
  11.  * Plugin URI: https://wordpress.org/plugins/hello-dolly/
  12.  * Description: This is not just a plugin, it symbolizes the hope and enthusiasm of an entire generation summed up in two words sung most famously by Louis Armstrong: Hello, Dolly. When activated you will randomly see a lyric from <cite>Hello, Dolly</cite> in the upper right of your admin screen on every page.
  13.  * Author: Matt Mullenweg
  14.  * Version: 1.6
  15.  * Author URI: http://ma.tt/
  16.  */ 
  17. /**
  18.  * Defines the lyrics for 'Hello Dolly'.
  19.  *
  20.  * @return string A random line of from the lyrics to the song.
  21.  */
  22. function hello_dolly_get_lyric() {
  23.     /** These are the lyrics to Hello Dolly */
  24.     $lyrics = "Hello, Dolly
  25. Well, hello, Dolly
  26. It's so nice to have you back where you belong
  27. You're lookin' swell, Dolly
  28. I can tell, Dolly
  29. You're still glowin', you're still crowin'
  30. You're still goin' strong
  31. We feel the room swayin'
  32. While the band's playin'
  33. One of your old favourite songs from way back when
  34. So, take her wrap, fellas
  35. Find her an empty lap, fellas
  36. Dolly'll never go away again
  37. Hello, Dolly
  38. Well, hello, Dolly
  39. It's so nice to have you back where you belong
  40. You're lookin' swell, Dolly
  41. I can tell, Dolly
  42. You're still glowin', you're still crowin'
  43. You're still goin' strong
  44. We feel the room swayin'
  45. While the band's playin'
  46. One of your old favourite songs from way back when
  47. Golly, gee, fellas
  48. Find her a vacant knee, fellas
  49. Dolly'll never go away
  50. Dolly'll never go away
  51. Dolly'll never go away again"; 
  52.     // Here we split it into lines.
  53.     $lyrics = explode( "\n", $lyrics );
  54.     // And then randomly choose a line.
  55.     return wptexturize( $lyrics[ mt_rand( 0, count( $lyrics ) - 1 ) ] );
  56. } 
  57. add_action( 'admin_notices', 'hello_dolly' );
  58. /**
  59.  * This just echoes the chosen line, we'll position it later. This function is
  60.  * set up to execute when the admin_notices action is called.
  61.  */
  62. function hello_dolly() {
  63.     $chosen = hello_dolly_get_lyric();
  64.     echo "<p id='dolly'>$chosen</p>"; // WPCS: XSS OK.
  65. } 
  66. add_action( 'admin_head', 'dolly_css' );
  67. /**
  68.  * Add some CSS to position the paragraph.
  69.  */
  70. function dolly_css() { 
  71.     /**
  72.      *This makes sure that the positioning is also good for right-to-left languages.
  73.      */
  74.     $x = is_rtl() ? 'left' : 'right';
  75.     echo "<style type='text/css'> #dolly { float: $x; padding-$x: 15px; padding-top: 5px; margin: 0; font-size: 11px; } </style> "; // WPCS: XSS OK. 
  76. }
Notice that the plugin continues to work and the code is a bit cleaner. Lastly, let's verify that this passes the PHP CodeSniffer test. Let's re-run the code that we used above to initially evaluate the plugin.
  1. $ vendor/bin/phpcs --standard=WordPress hello-dolly
And the output that we see:
  1. Skyhopper5:tutsplus_demo tommcfarlin$
Exactly: There should be no output. Instead, it should be a return to the standard command prompt.

Excellent. The plugin has been brought up to standard. This is why having a code sniffer is so valuable: It finds the errors in your code based on the rules you define and then reports any errors that may exist.

Ultimately, this ensures that you're releasing the highest quality written code into a production-level site. Now, this does not mean you should avoid unit testing or other types of testing, nor does this mean bugs don't exist. It just means that your code is up to a high standard.

Conclusion

And with that, we conclude the series on using PHP CodeSniffer. Recall that throughout the series, we have covered the idea of code smells, how to refactor them, and what tools are available to us when working with PHP applications.

In this article, we saw how we can use a provided set of rules for the WordPress Coding Standards to evaluate our code while working on a new or an existing project. Note that some IDEs support the ability to execute the rules while writing code.

Although that's beyond the scope of this particular tutorial, you can find resources for this in various places throughout the web.
Written by Tom McFarlin

If you found this post interesting, follow and support us.
Suggest for you:

Learning PHP 7: From the Basics to Application Development

The Complete PHP 7 Guide for Web Developers

Up to Speed with PHP 7

PHP MySQL Database Connections

The Complete PHP with MySQL Developer Course (New)

Saturday, August 6, 2016

Using PHP CodeSniffer With WordPress: Installing and Using the WordPress Rules_part1

Prerequisites

This should be a very short list. If you've followed along with the series up to this point, you need to have:
  • a version of PHP (preferably 5.6.10 or later)
  • PHP CodeSniffer
  • Composer
All of this is covered in detail throughout the previous articles in the series, but if you've gotten this far and are comfortable with the command line then this should be a cinch in comparison to what we've done so far.

With all of that said, let's get started.

The WordPress Rules for PHP CodeSniffer

First, locate the WordPress Coding Standards rules on GitHub. They're easy to find.


You can read all about the details of the project from the project page, but the most important thing I'd like to share is as follows:
This project is a collection of PHP_CodeSniffer rules (sniffs) to validate code developed for WordPress. It ensures code quality and adherence to coding conventions, especially the official WordPress Coding Standards.
I'd like to bring your attention to the phrase that this references the "official WordPress Coding Standards." Note that these rules are based on the WordPress Coding Standards. That is, you can't officially reference them.

If you're looking to find a way to look through the rules that WordPress defines, check out this article in the Codex. It's easy to follow, easy to read, but a lot to remember. Thankfully, we have the rule set linked above.

The important thing to note is that even if you aren't familiar with the rules, the CodeSniffer will find the problems with your code and will notify you of what you need to fix. Though you don't have to read the Codex article, it can sometimes help in identifying what's needed based on the errors or warnings the sniffer generates.

1. Install the WordPress Rules
Assuming you've properly installed PHP CodeSniffer, let's add the WordPress rules to the software. For this tutorial, I'm going to do everything via the command line so as to be as platform agnostic as possible. I'll offer a few words regarding IDEs and rules at the end of the series.

Open your Terminal and navigate to where you have your copy of PHP CodeSniffer installed. If you've been following along with this series of tutorials, then you likely recall we have a composer.json file that will pull this in for us. If not, remember to create composer.json in the root of your project and add this to the file:
  1. {
  2.     "require": {
  3.         "squizlabs/php_codesniffer": "2.*"
  4.     }
  5. }
Once done, run $ composer update from your Terminal and you'll have everything you need to get going. To verify the installation, run the following command:
  1. $ vendor/bin/phpcs --version
And you should see something like the following output:
  1. PHP_CodeSniffer version 2.6.0 (stable) by Squiz (http://www.squiz.net)
Perfect. Next, let's install the WordPress rules. Since we're using Composer (and will continue to do so), this is very easy to do.

Run the following command from within the root directory of your project:
  1. composer create-project wp-coding-standards/wpcs:dev-master --no-dev
Note that you may be prompted with the following question:
  1. Do you want to remove the existing VCS (.git, .svn..) history? [Y,n]?
If you know what you're doing, then feel free to go ahead and select 'n'; otherwise, you'll be fine hitting 'y'.

2. Add the Rules to PHP CodeSniffer
Now that PHP CodeSniffer is installed, and the WordPress rules are installed, we need to make sure PHP CodeSniffer is aware of our new ruleset. To do this, we need to enter the following command in the command line.

From the root of your project directory, enter the following command:
  1. $ vendor/bin/phpcs --config-set installed_paths wpcs
To verify that the new rules have been added, we can ask PHP CodeSniffer to report to us the sets of rules that it currently has available. In the Terminal, enter the following command:
  1. $ vendor/bin/phpcs -i
And you should see the following output (or something very similar):
  1. The installed coding standards are MySource, PEAR, PHPCS, PSR1, PSR2, Squiz, Zend, WordPress, WordPress-Core, WordPress-Docs, WordPress-Extra and WordPress-VIP
Notice in the line above that we have several sets of rules regarding WordPress. Pretty neat, isn't it? Of course, let's see how this stacks up when we run the rule sets against a plugin like Hello Dolly.
Written by Tom McFarlin

If you found this post interesting, follow and support us.
Suggest for you:

Learning PHP 7: From the Basics to Application Development

The Complete PHP 7 Guide for Web Developers

Learn PHP 7 This Way to Rise Above & Beyond Competion!

PHP MySQL Database Connections

The Complete PHP with MySQL Developer Course (New)


Friday, August 5, 2016

Building Your Startup: Customizing the Meeting View_part2(end)

Initializing New Meeting Sessions

Whenever the user creates a new meeting, we have to load their default settings and copy them to the individual meeting's settings. initializeMeetingSetting is called when a new meeting is created to do this:
  1. public function initializeMeetingSetting($meeting_id,$owner_id) {
  2.   // load meeting creator (owner) user settings to initialize meeting_settings
  3.   $user_setting = UserSetting::find()->where(['user_id' => $owner_id])->one();
  4.   $meeting_setting = new MeetingSetting();
  5.   $meeting_setting->meeting_id = $meeting_id;
  6.   $meeting_setting->participant_add_place=$user_setting->participant_add_place;
  7.   $meeting_setting->participant_add_date_time=$user_setting->participant_add_date_time;
  8.   $meeting_setting->participant_choose_place=$user_setting->participant_choose_place;
  9. $meeting_setting->participant_choose_date_time=$user_setting->participant_choose_date_time;
  10.   $meeting_setting->participant_finalize=$user_setting->participant_finalize; 
  11.   $meeting_setting->save();
  12. }
With meeting settings in place, we're ready to move on to what's actually the bulk of today's work, customizing the meeting views for the owner and participant.

Reviewing the Meeting Owner View
Now, let's consider the state of our meeting view based on the meeting creator or owner. Here's a meeting invitation I've recently created to invite my friend Rob to drinks:


The Command Bar

Before Send and Finalize should be enabled, there must be a person invited and at least one place and time. If there are more than one place and time, one must be chosen for the meeting to be finalized.

The Cancel (X icon) and Edit (pencil icon) meeting buttons are also enabled for creators.

People

For the MVP, we're limiting meeting invitations to one participant at first. So, once a person has been invited, the Add (plus icon) button is disabled.

Places and Date & Times

The creator can add Places and Date & Times up to our site's maximum (e.g. seven per meeting) and they can indicate their availability and acceptance. And, finally, when there is more than one, they can choose which location and time will be used.

Notes

The creator can always add notes to the meeting. Notes allow the creator and participants to communicate with each other.

Ultimately, we'll put the bulk of our work into improving the AJAX functionality so that as the owner chooses places and times, the Send and Finalize buttons are properly enabled (or disabled in some cases).

Here's an example of a meeting with two possible times. The Finalize button can't be enabled until one time is chosen:


Once the choice is made, we'd like to enable the Finalize button via AJAX, sparing the user a page refresh.

Reviewing the Participant View
When we view the invitation from the participant's point of view, there's a lot less initial capability:


The participant can cancel (X icon) their attendance to the meeting and they can specify whether the places and times are acceptable to them, but they can't choose the final place or Finalize the meeting. Also, the data in the You and Them columns are now switched. And, the participant panel is hidden as it's not needed.

Additionally, let's say the meeting was created with settings that allowed the participant to choose the location, date and time but not finalize the meeting. That would need to look like this:


Since there's only one Place, Herkimer Coffee, there's no need for a choice selector. But, where there are two possible times, you can now see the Choose selectors. Still, there is no Finalize button.

It turned out that supporting all of this required a lot of new code to update the system, but this is beginning to dive into the heart of the product—the scheduling meetings user experience. I'll walk you through a handful of the changes that were needed.

Coding the Meeting Requirements
Implementing the Meeting Settings

In the meeting-time and meeting-place panels, we need to use the meeting settings to determine if we need to show the choice selector. In the _panel.php view, it looks like this:
  1. <table class="table">
  2.      <thead>
  3.      <tr class="small-header">
  4.        <td></td>
  5.        <td ><?=Yii::t('frontend','You') ?></td>
  6.        <td ><?=Yii::t('frontend','Them') ?></td>
  7.        <td >
  8.          <?php
  9.           if ($timeProvider->count>1 && ($isOwner || $model->meetingSettings->participant_choose_date_time)) echo Yii::t('frontend','Choose');
  10.          ?>
  11.         </td>
  12.     </tr>
  13.     </thead>
  14.     <?= ListView::widget([ 
  15.            'dataProvider' => $timeProvider, 
  16.            'itemOptions' => ['class' => 'item'], 
  17.            'layout' => '{items}',
  18.            'itemView' => '_list', 
  19.            'viewParams' => ['timeCount'=>$timeProvider->count,'isOwner'=>$isOwner,'participant_choose_date_time'=>$model->meetingSettings['participant_choose_date_time']],           
  20.        ]) ?>
  21.   </table>
We're checking the participant settings and passing them as a parameter to the subsequent _list.php view, which looks like this:
  1. <td style>
  2.       <?php
  3.       if ($timeCount>1) {
  4.         if ($model->status == $model::STATUS_SELECTED) {
  5.             $value = $model->id;
  6.         }    else {
  7.           $value = 0;        
  8.         }      
  9.         if ($isOwner || $participant_choose_date_time) {
  10.           // value has to match for switch to be on
  11.           echo SwitchInput::widget([
  12.               'type' => SwitchInput::RADIO,
  13.               'name' => 'time-chooser',
  14.               'items' => [
  15.                   [ 'value' => $model->id],
  16.               ],
  17.               'value' => $value,
  18.               'pluginOptions' => [  'size' => 'mini','handleWidth'=>60,'onText' => '<i class="glyphicon glyphicon-ok"></i>','offText'=>'<i class="glyphicon glyphicon-remove"></i>'],
  19.               'labelOptions' => ['style' => 'font-size: 12px'],
  20.           ]);            
  21.         }
  22.       }
  23.       ?>
  24.   </td>
If the view is the creator or the participant is allowed to choose the final time, they'll see something like this, the ability to Choose in the right column:


Can the Viewer Send and Finalize the Meeting

I created canSend() and canFinalize() functions, which support the code generally and the AJAX requests to determine the active state of Send and Finalize buttons.
Here's canSend():
  1. public function canSend($sender_id) {
  2.        // check if an invite can be sent
  3.        // req: a participant, at least one place, at least one time
  4.        if ($this->owner_id == $sender_id      
  5.         && count($this->participants)>0
  6.         && count($this->meetingPlaces)>0
  7.         && count($this->meetingTimes)>0
  8.         ) {
  9.          $this->isReadyToSend = true;
  10.        } else {
  11.          $this->isReadyToSend = false;
  12.        }
  13.        return $this->isReadyToSend;
  14.       }
The organizer can't send the meeting invitation until there are participant(s), places and times.
Here's canFinalize():
  1. public function canFinalize($user_id) {
  2.         $this->isReadyToFinalize = false;
  3.         // check if meeting can be finalized by viewer
  4.         // check if overall meeting state can be sent by owner
  5.          if (!$this->canSend($this->owner_id)) return false;
  6.           $chosenPlace = false;
  7.           if (count($this->meetingPlaces)==1) {
  8.             $chosenPlace = true;
  9.           } else {
  10.             foreach ($this->meetingPlaces as $mp) {
  11.               if ($mp->status == MeetingPlace::STATUS_SELECTED) {
  12.                 $chosenPlace = true;
  13.                 break;
  14.               }
  15.             }
  16.           }
  17.           $chosenTime = false;
  18.           if (count($this->meetingTimes)==1) {
  19.             $chosenTime = true;
  20.           } else {
  21.             foreach ($this->meetingTimes as $mt) {
  22.               if ($mt->status == MeetingTime::STATUS_SELECTED) {
  23.                   $chosenTime = true;
  24.                   break;
  25.               }                
  26.             }
  27.           }
  28.           if ($this->owner_id == $user_id || 
  29.           $this->meetingSettings->participant_finalize) {
  30.             if ($chosenPlace && $chosenTime) {
  31.               $this->isReadyToFinalize = true;              
  32.             }
  33.           }                    
  34.         return $this->isReadyToFinalize;
  35.       }
This first checks if the meeting could be sent, because if not, it can't be finalized. Then, it checks to make sure that both a place and time have been chosen. And then, it checks if the viewer is the organizer or the meeting settings allow a participant to finalize the meeting.

Basically, as changes are made, you'll see the state of the Send and Finalize buttons change:


In the meeting view.php, I've embedded JavaScript to support AJAX updates to the state of the Send and Finalize buttons as users change settings for their meeting. When selections of places and times are made, refreshSend() and refreshFinalize() are called and the buttons are appropriately modified:
  1. <?php
  2. if (isset(Yii::$app->params['urlPrefix'])) { 
  3.   $urlPrefix = Yii::$app->params['urlPrefix'];
  4.   } else {
  5.     $urlPrefix ='';
  6.   }
  7. $script = <<< JS
  8. function refreshSend() {
  9.   $.ajax({
  10.      url: '$urlPrefix/meeting/cansend',   
  11.      data: {id: $model->id, 'viewer_id': $viewer},
  12.      success: function(data) {
  13.        if (data)
  14.          $('#actionSend').removeClass("disabled");
  15.         else
  16.         $('#actionSend').addClass("disabled");
  17.        return true;
  18.      }
  19.   });
  20. }
  21.  
  22. function refreshFinalize() {
  23.   $.ajax({
  24.      url: '$urlPrefix/meeting/canfinalize',   
  25.      data: {id: $model->id, 'viewer_id': $viewer},
  26.      success: function(data) {
  27.        if (data)
  28.          $('#actionFinalize').removeClass("disabled");
  29.         else
  30.         $('#actionFinalize').addClass("disabled");
  31.        return true;
  32.      }
  33.   });
  34. }
  35.  
  36. JS;
  37. $position = \yii\web\View::POS_READY;
  38. $this->registerJs($script, $position);
  39. ?>
Reversing the Place and Time Status Selectors

In the current user interface, we show the viewer's place and time selections in the leftmost or first column. The code has to be customized to reverse this when participants are viewing:


To support showing different data in the You and Them columns of the meeting view for Times and Places, the meeting-time and meeting-place _list.php files needed to be updated to dynamically determine what data to display:
  1. <td style>
  2.     <?php
  3.        if ($isOwner) {
  4.          showTimeOwnerStatus($model,$isOwner);
  5.        } else {
  6.          showTimeParticipantStatus($model,$isOwner);
  7.        }
  8.     ?>
  9.   </td>
  10.   <td style>
  11.       <?php
  12.         if (!$isOwner) {
  13.            showTimeOwnerStatus($model,$isOwner);
  14.          } else {
  15.            showTimeParticipantStatus($model,$isOwner);
  16.          }
  17.       ?>
  18.   </td>
For now, I placed these functions within the _panel.php view, which calls _list.php, as they rely on having the SwitchInput widget included in context:
  1. <?php
  2. use \kartik\switchinput\SwitchInput;
  3.  
  4.   function showTimeOwnerStatus($model,$isOwner) {
  5.     foreach ($model->meetingTimeChoices as $mtc) {
  6.       if ($mtc->user_id == $model->meeting->owner_id) {
  7.           if ($mtc->status == $mtc::STATUS_YES)
  8.             $value = 1;
  9.           else
  10.             $value =0;
  11.             echo SwitchInput::widget([
  12.             'type' => SwitchInput::CHECKBOX,              
  13.             'name' => 'meeting-time-choice',
  14.             'id'=>'mtc-'.$mtc->id,
  15.             'value' => $value,
  16.             'disabled' => !$isOwner,
  17.             'pluginOptions' => ['size' => 'mini','onText' => '<i class="glyphicon glyphicon-ok"></i>','offText'=>'<i class="glyphicon glyphicon-remove"></i>','onColor' => 'success','offColor' => 'danger',],
  18.             ]);          
  19.       }
  20.     }
  21.   }
  22.  
  23.   function showTimeParticipantStatus($model,$isOwner) {
  24.     foreach ($model->meetingTimeChoices as $mtc) {
  25.       if (count($model->meeting->participants)==0) break;
  26.       if ($mtc->user_id == $model->meeting->participants[0]->participant_id) {
  27.           if ($mtc->status == $mtc::STATUS_YES)
  28.             $value = 1;
  29.           else if ($mtc->status == $mtc::STATUS_NO)
  30.             $value =0;
  31.           else if ($mtc->status == $mtc::STATUS_UNKNOWN)
  32.             $value =-1;
  33.           echo SwitchInput::widget([
  34.             'type' => SwitchInput::CHECKBOX,          
  35.             'name' => 'meeting-time-choice',
  36.             'id'=>'mtc-'.$mtc->id,
  37.             'tristate'=>true,
  38.             'indeterminateValue'=>-1,
  39.             'indeterminateToggle'=>false,
  40.             'disabled'=>$isOwner,
  41.             'value' => $value,
  42.             'pluginOptions' => ['size' => 'mini','onText' => '<i class="glyphicon glyphicon-ok"></i>','offText'=>'<i class="glyphicon glyphicon-remove"></i>','onColor' => 'success','offColor' => 'danger',],
  43.         ]);          
  44.       }
  45.     }  
  46.   }
  47. ?>
Upcoming Adjustments
Ultimately, there are a lot of improvements to make to this code going forward. In places, I'm making AJAX calls to the server two or three times when I could code these more efficiently into a single request. In other places, I can do more locally with JavaScript. And the user interface will need to keep improving, and the code will need to change to adapt to that. But, from a functional perspective, today's work represents a lot of overall progress towards the MVP.
Written by Jeff Reifman

If you found this post interesting, follow and support us.
Suggest for you:

Learning PHP 7: From the Basics to Application Development

The Complete PHP 7 Guide for Web Developers

Learn PHP 7 This Way to Rise Above & Beyond Competion!

PHP MySQL Database Connections

The Complete PHP with MySQL Developer Course (New)