-
Using Data Providers in PHPUnit Tests
This is the before code, where a single test is run, and each scenario I’m testing could be influenced by the previous scenario (not a good thing, unless that was my goal, which it was not).
123456789101112131415161718192021/*** Tests the getRegistrationsWithinNumDays function.*/public function testGetRegistrationsWithinNumDays() {$firstRegDateTimeStr = '2024-03-18 08:00:00';$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-18 09:00:00']);$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-19 13:00:00']);$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-20 14:20:00']);$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-20 17:35:00']);$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-21 17:35:00']);$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-22 08:00:00']);$registrations = Utils::getIdsOfRegistrationsWithinNumDays(1, $firstRegDateTimeStr);$this->assertEquals(2, count($registrations));$registrations = Utils::getIdsOfRegistrationsWithinNumDays(2, $firstRegDateTimeStr);$this->assertEquals(3, count($registrations));$registrations = Utils::getIdsOfRegistrationsWithinNumDays(3, $firstRegDateTimeStr);$this->assertEquals(5, count($registrations));}This is the after code, where three tests are run. Unfortunately this takes 3x longer to execute. The upside is that each scenario cannot affect the others and it’s perhaps more readable and easier to add additional scenarios.
123456789101112131415161718192021222324252627/*** Data provider for testGetRegistrationsWithinNumDays.*/public function registrationsDataProvider() {return [// Label => [numDays, expectedCount].'1 day' => [1, 2],'2 days' => [2, 3],'3 days' => [3, 5],];}/*** Tests the getRegistrationsWithinNumDays function using a data provider.** @dataProvider registrationsDataProvider*/public function testGetRegistrationsWithinNumDays($numDays, $expectedCount) {$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-18 08:00:00']);$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-19 09:00:00']);$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-20 13:00:00']);$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-21 14:20:00']);$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-21 17:35:00']);$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-22 17:35:00']);$registrations = Utils::getIdsOfRegistrationsWithinNumDays($numDays, '2024-03-18 08:00:00');$this->assertEquals($expectedCount, count($registrations), "Failed for {$numDays} days.");} -
Rendering null boolean values with Grid4PHP
I’m a big fan of Grid4PHP for adding a quick CRUD user interface to a MySQL database.
I have a lot of boolean columns, like “is_telehealth” that need to start as NULL until we make a determination about the field’s value. Grid4PHP renders null booleans as “0” in the table/edit form. This is misleading, given it’s not actually a 0 value. If you add the ‘isnull’ attribute to the column it lets you save the null value, but it still renders as a “0”.
Using a custom formatter I was able to render an empty string (nothing) instead of “0” for these null booleans:
1234$boolNullFormatter = "function(cellval,options,rowdata){ return cellval === null ? '' : cellval; }";$cols[] = ['name' => 'is_in_gmb', 'isnull' => true, 'formatter' => $boolNullFormatter];$cols[] = ['name' => 'is_property', 'isnull' => true, 'formatter' => $boolNullFormatter];$cols[] = ['name' => 'is_telehealth', 'isnull' => true, 'formatter' => $boolNullFormatter]; -
Testing Cookie Modification in Laravel 8
If there’s a better way to pass a cookie from one request to another, in a phpunit feature test in Laravel, please let me know! Here’s one way to handle it:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546<?phpnamespace Tests\Feature;use Illuminate\Cookie\CookieValuePrefix;use Tests\TestCase;class DiveDeeperTest extends TestCase{/*** Tests if the diveDeeper route appends the passed value to the cookie arr.* @return void*/public function test_it_updates_path_cookie(){// Post a deeperPath to the diveDeeper route (with a starter cookie).// This should append the "deeperPath" value to the "path" cookie array.$resp = $this->withCookie('path', serialize(['/']))->post(route('diveDeeper'), ['deeperPath' => 'Users']);// Assert that the cookie has been modified.$resp->assertCookie('path', serialize(['/', 'Users']));// If we make another post request here, it won't include our// modified "path" cookie. We have to retrieve it, then pass it// through to the next request.// Decrypt the cookie (now modified by the diveDeeper route).$cookieValue = CookieValuePrefix::remove(app('encrypter')->decrypt($resp->getCookie('path')->getValue(), false));// Hit the diveDeeper route again using the modified cookie value.$resp = $this->withCookie('path', $cookieValue)->post(route('diveDeeper'), ['deeperPath' => 'adam']);// Assert that the cookie has been modified again.$resp->assertCookie('path', serialize(['/', 'Users', 'adam']));// Etc...}} -
Any Random Saturday Using Faker
Here’s a quick one, folks. I’m using Faker in Laravel factories to generate realistic data. I have a “week end date” field in one of my models that must always be a Saturday. Here’s how I generate a unique random Saturday:
1date('Y-m-d', strtotime('next Saturday ' . $this->faker->unique->date())) -
Using Carbon to Generate Random Dates and Times
There are a number of ways to generate random dates/times in PHP. Here are some examples using the Carbon library:
1234567891011// Between now and 180 days agoCarbon::today()->subDays(rand(0, 180))> 2021-09-02 00:00:00> 2021-06-23 00:00:00> 2021-05-23 00:00:00// Between now and 180 days ago with random timeCarbon::today()->subDays(rand(0, 179))->addSeconds(rand(0, 86400));> 2021-06-11 22:35:03> 2021-10-24 13:19:53> 2021-06-09 20:47:44 -
Remote PHP Debugging with Xdebug + PHPStorm or VSCode on Cloudways
Here’s a quick breakdown of the steps required to debug a PHP site on a remote Cloudways server.
Step 1:
Enable xdebug for the whole Cloudways server:
Server » Settings & Packages » Advanced » XDEBUG: Enabled
Step 2:
For the specific application in Cloudways, add some PHP settings:
Application » Application Settings » PHP FPM Settings:
123php_value[xdebug.mode] = debugphp_value[xdebug.start_with_request] = yesphp_value[xdebug.remote_connect_back] = 1Step 3: Setup SSH config for an ssh tunnel to the server:
- Edit
~/.ssh/config
1234Host mhd1_xdebugHostName 44.33.222.111User master_acevddddddRemoteForward 9003 localhost:9003Step 4 (PHPStorm):
Configure PHPStorm Preferences
- PHP » Debug » Xdebug » Debug port = 9003
- PHP » Debug » Xdebug — Check all four boxes:
- Can accept external connections
- Resolve breakpoint if it’s not available on the current line
- Force break at first line when no path mapping specified
- Force break at first line when a script is outside the project
- You shouldn’t need to configure servers, PHP cli, deployment locations, or anything similar…
Try It
- Start an SSH tunnel:
ssh mhd1_xdebug
- Drop a breakpoint in index.php (or whatever will surely execute)
- Visit the site, then wait for PHPStorm to prompt you for which file to connect to. Choose accordingly. It should connect and pause. You only have to do this once; it’ll remember the settings and path mapping in PHP » Servers.
Step 4 (VSCode)
Configure VSCode Preferences
- Install https://marketplace.visualstudio.com/items?itemName=felixfbecker.php-debug
- Click the Run and Debug sidebar icon (⌘⇧D)
- SSH into the server to find the exact path to the root of the site (which should match the root of your repo locally, your workspace folder)
- Add new configuration (you can disable the log option if things work well right away)
123456789101112131415{"version": "0.2.0","configurations": [{"name": "Listen for Xdebug","type": "php","request": "launch","port": 9003,"pathMappings": {"/home/511111.cloudwaysapps.com/crwysaaaaa/public_html": "${workspaceFolder}",},"log": true},]}Try it
- Start an SSH tunnel:
ssh mhd1_xdebug
- Click Listen for Xdebug in the Run and Debug screen
- Drop a breakpoint in index.php (or whatever will surely execute)
- Visit the site
- Edit
-
Basic HTTP Authentication in Drupal Site Using settings.php
Here’s a quick and painless way of preventing public access to a Drupal site using settings.php (or settings.local.php).
I’ve been using this for development and staging sites that I want to keep private.
If you want this to be available to all settings*.php files you should put this near the top of your settings.php file:
123456789101112131415161718192021222324/*** Locks the site via basic http auth.** D7: if (!drupal_is_cli())* D8: if (PHP_SAPI !== 'cli')** @param array $users* Array of users e.g., ['user1' => 'pass1', 'user2' => 'pass2'].** @see https://agileadam.com/2018/04/basic-http-auth-drupal-site-using-settings-php/*/function lock_with_basicauth($users) {if (PHP_SAPI !== 'cli') {$valid_users = $users;$valid_usernames = array_keys($valid_users);$user = (!empty($_SERVER['PHP_AUTH_USER'])) ? $_SERVER['PHP_AUTH_USER'] : '';$pass = (!empty($_SERVER['PHP_AUTH_PW'])) ? $_SERVER['PHP_AUTH_PW'] : '';if (!((in_array($user, $valid_usernames)) && ($pass == $valid_users[$user]))) {header('WWW-Authenticate: Basic realm="Private Site"');header('HTTP/1.0 401 Unauthorized');die('Not authorized.');}}}Then, you can leverage it wherever you’d like. For example, on an Acquia site I might add this to the bottom of settings.php:
12345678910if (!empty($_ENV['AH_SITE_ENVIRONMENT'])) {switch ($_ENV['AH_SITE_ENVIRONMENT']) {case 'dev':lock_with_basicauth(['agileadam' => 'mysecretdevpass']);break;case 'test':lock_with_basicauth(['agileadam' => 'mysecretstagepass']);break;}}For non-Acquia sites I’d call the function at the bottom of settings.local.php.
-
Setting up OCI8 (PHP Oracle module) on Webfaction
Webfaction, my favorite web host, allows you to compile PHP modules in your home directory for use on your websites. Here’s the process for configuring OCI8 to talk to Oracle databases:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748# Make directoriesmkdir -p ~/lib/php71_exts/mkdir -p ~/lib/php71_exts/instantclientcd ~/lib/php71_exts# Get the php modulewget https://pecl.php.net/get/oci8-2.1.8.tgztar xvf oci8-2.1.8.tgzcd oci8-2.1.8phpize71# Get Oracle Instant Client (use your local computer)# http://www.oracle.com/technetwork/topics/linuxx86-64soft-092277.html# SSH the files to the serverscp instantclient*.zip web553:~/lib/php71_exts/# Extract files and symlink a few modulesmv instantclient*.zip instantclient/cd instantclient/unzip instantclient-basic-linux.x64-12.2.0.1.0.zipunzip instantclient-sqlplus-linux.x64-12.2.0.1.0.zipunzip instantclient-sdk-linux.x64-12.2.0.1.0.zipcd instantclient_12_2/ln -s libclntsh.so.12.1 libclntsh.soln -s libocci.so.12.1 libocci.so# Configure the php modulecd ~/lib/php71_exts/oci8-2.1.8/./configure --with-oci8=shared,instantclient,/home/yourname/lib/php71_exts/instantclient/instantclient_12_2 -with-php-config=/usr/local/bin/php71-configmakemv modules/oci8.so ~/lib/php71_exts/# Enable the modulecd ~/webapps/mysitels /home/yourname/lib/php71_exts/oci8.sols -alh /home/yourname/lib/php71_exts/oci8.sovim php.iniextension_dir=/home/yourname/lib/php71_extsextension=/home/yourname/lib/php71_exts/oci8.so# Test# Create a file (e.g., index.php) to call phpinfo().# Load the page and check if the OCI module is loaded.# Cleanupcd ~/lib/php71_exts/rm package.xml oci8-2.1.8.tgz -
Tideways and Xhgui using DevDesktop and Docker
THIS POST IS UNFINISHED. Use at your own risk. I needed to share with a colleague, so I’m just getting it out into the world. Your Mileage May Vary!
I’ve been working on some large Drupal 8 migrations and have been wanting to profile them for some time due to a few migrations taking far more time than I expected. Acquia DevDesktop, which I happen to be using for this site, offers xhprof in the older PHP environments, but I wanted to get something setup in PHP 7.
For PHP 7 the recommendation is to use Tideways; it’s a modern fork of xhprof. After collecting (tracing/profiling) the data with Tideways you need a way to analyze the data. I used a Docker environment to get Xhgui running. Here’s what a few xhgui screens look like. The best part is that nearly everything is clickable so you can drill down to figure out what’s slow, and why!
-
Setting up xdebug for PHP 7 in Acquia DevDesktop
The Acquia DevDesktop help page says:
The PHP 7 version currently included with Acquia Dev Desktop does not currently include Xdebug. You can download an updated version of Xdebug here .
Here are the actual steps I used. YMMV.
123456789101112131415161718192021cd /Applications/DevDesktop/php7_0/extwget http://xdebug.org/files/xdebug-2.5.5.tgztar xvf xdebug-2.5.5.tgzrm xdebug-2.5.5.tgzcd xdebug-2.5.5/Applications/DevDesktop/php7_0/bin/phpize./configure --enable-xdebug CC="gcc -arch i386" CXX="g++ -arch i386" -with-php-config=/Applications/DevDesktop/php7_0/bin/php-configmakemv modules/xdebug.so ../cd ..rm -rf package.xml xdebug-2.5.5# Edit /Applications/DevDesktop/php7_0/bin/php.ini# Add these lines:zend_extension="/Applications/DevDesktop/php7_0/ext/xdebug.so"xdebug.remote_enable=1xdebug.remote_log=/tmp/xdebug.log# Restart DevDesktop services