For the last few days I have been struggling, sort of, to find the best way to implement a configuration class or object that will allow me to continually append values to it and allow easy access to the members of it. Think of it as a sort of data store that houses multi-tiered data that is logically structured. For example, lets say I have a configuration array like this:
<?php
$array = array(
'defaults' => array(
'page' => 'index',
'action' => 'default',
),
'routes' => array(
'controller',
'view',
'model',
),
'application' => 'Bilbo',
'nested' => array(
'levels' => array(
1 => 'floor',
'garage',
'canopy',
),
'names' => array(
1 => 'Hectar',
'Jonas',
'Phil',
),
'named' => array(
'floor' => 'Hectar',
'garage' => 'Jonas',
'canopy' => 'Phil',
),
),
);
?>
And lets say that this array is parsed into a config object that is in essence an interface so that config params can be set and got from it. The config array itself is protected so it cannot be manipulated directly (because seriously, why should you be able to manipulate configuration settings directly?). What if I wanted to find the value for $array['defaults']['page']? Without being able to access the array directly you cannot easily do this without writing function that would somehow recurse the array until it finds the correctly nested key that matches you request and returns its value, if it is found.
That is one idea. And I tried that. A lot. And as I went through trying that I began to realize that if I have a 200 member config array (say it has been continually built up) and I am accessing that from 20 different sources per application (think a framework hitting itself and a framework-hooked client hitting it as well) the the config array would essentially be traversed for each request coming in to the object. As I thought about this I began to think that it might be easier to traverse the array once and set the array into a levelized series of keys and values that map to each level of the config array so that a simple request to something like config::fetch('defaults:page'); would return ‘index’ (based on our example array).
So I set out to do that and I came up with something that has been immeasurably useful to me already. Essentially it is a class that allows for the setting of config parameters into the class in a way that makes each level accessible by its current location in the tree, separated by a delimiter (in my case the colon ‘:’). I know the following code needs a little more love, but it is doing what I want it to at the moment (I will be making changes that allow setting a custom delimiter and the whatnot):
<?php
/**
* Padlock - The PHP Application Developer's Library of Objects and Code Kits
*
* @category Padlock
* @package Padlock
* @author Robert Gonzalez <robert@everah.com>
* @license PLEASE SEE ACCOMPANYING LICENSE TEXT OR THE {@see Padlock::license()} method
* @version $Id: Config.php 33 2008-08-20 06:30:39Z robert $
*/
/**
* Padlock Configuration object abstract
*
* The Padlock Configuration object handles parsing of initial framework instructions
* and individual configuration items. This class is designed to be extended by
* concrete classes that are specific to a particular type of config implementation.
*
* @author Robert Gonzalez <robert@everah.com>
* @category Padlock
* @package Padlock
* @version @package_version@
*/
abstract class Padlock_Config {
/**
* String type config param constant
*/
const CONFIG_TYPE_STRING = 'string';
/**
* Array type config param constant
*/
const CONFIG_TYPE_ARRAY = 'array';
/**
* Object type config param constant
*/
const CONFIG_TYPE_OBJECT = 'object';
/**
* Flag that tells the configuration class whether to merge new values or not
*
* @access protected
* @var boolean
*/
protected static $_merge = true;
/**
* The framework configuration array
*
* @access protected
* @var array
*/
protected static $_config = array();
/**
* The framework configuration array in the raw
*
* @access protected
* @var array
*/
protected static $_configRaw = array();
/**
* Object constructor
*
* This is final and private - basicaly this class is meant to never be
* instantiated.
*
* @access private
*/
final private function __construct() {
trigger_error('The Config object cannot be instantiated', E_USER_ERROR);
}
/**
* Abstracted child method that requires definition within child classes
*
* @access public
* @param mixed $config Source of the configuration collection
*/
abstract public static function setFrom($config);
/**
* Sets up and loads a configuration collection into the framework
*
* If the $config param is an array the array will be loaded into the config
* class as a merge. It was also be traversed and exploded out into each
* component of the array as a map to the end element value of the array
* path.
*
* If the $config param is an object the object will be traversed as an
* array and set into the config class as an array and a mapped array.
*
* If the $config param is a string it will be treated as though it were a
* file name and handled accordingly.
*
* @access public
* @param mixed $config Configuration to process
*/
public static function setup($config) {
switch(gettype($config)) {
case self::CONFIG_TYPE_ARRAY:
case self::CONFIG_TYPE_OBJECT:
Padlock_Config_Array::setFrom($config);
break;
case self::CONFIG_TYPE_STRING:
Padlock_Config_File::setFrom($config);
break;
}
// Set it now
self::_setConfig(self::$_configRaw);
}
/**
* Appends the config setup with more config params
*
* @access public
* @param array $config More config params to set, in the form of an array
*/
public static function append($config) {
self::_setRaw($config);
self::_setConfig(self::$_configRaw);
}
/**
* Sets a single config property and value
*
* This method will not set null values as null values have special meaning
* throughout the framework. Essentially if the value is null then it means
* there is no known config setting for the name/property. Nulls are returned
* when names/props cannot be found.
*
* @param string $name Config property to set
* @param mixed $value Value for this config property
*/
public static function set($name, $value, $merge = true) {
/**
* Before anything we do we must check a value.
*
* Null values are special to the config class in that a NULL will be
* literally translated to nonexitence. So if the value being passed is
* null any searching for it will return null anyway.
*/
if ($value === null) {
return false;
}
/**
* If we are merging OR if the config item is not currently set then we
* handle that first and call it done.
*/
if ($merge || self::$_merge || !self::has($name)) {
self::append(array($name => $value));
return true;
}
/**
* The only this could mean is that the value is null or the name
* already lives and is not being merged.
*/
return false;
}
/**
* Checks existence of a config property
*
* This will return true for null values of a property. This might appear
* counterintuitive until you consider that we are just checking if there
* is a property set in this object. Yes, the property can be set as a false
* or a null. It will be without value, but it will be set, which is what
* we are asking about.
*
* @access public
* @param string $name Config property to check
* @return boolean True if set, false otherwise
*/
public static function has($name) {
return is_string($name) && array_key_exists($name, self::$_config);
}
/**
* Gets a property value
*
* @access public
* @param string $name Property to get the value for
* @return mixed
*/
public static function get($name) {
// Return what is being asked for, or null
return self::has($name) ? self::$_config[$name] : null;
}
/**
* Fetches the entire configuration array from this class
*
* This is a useful convenience method meant to allow the fetching of the
* config settings array and use outside of this class. It can also be used
* to set these configs into other objects.
*
* @access public
* @return array Entire configuration array
*/
public static function fetch() {
return self::$_config;
}
/**
* Tells the class to turn merging of values on or off.
*
* This can be useful when the need to override or not override comes up as
* this method allows for changing of the config merge argument. By default
* this class will merge all set values.
*
* @access public
* @param boolean $on Flag setting to pass to the class
*/
public static function setMerge($on = true) {
self::$_merge = (bool) $on;
}
/**
* Sets a series of properties from an array of properties
*
* @access protected
* @param array $source Array to set values from
* @param string $key Name of the key to append to the config stack
*/
protected static function _setConfig($source, $key = null) {
// This should only be pushed if there is an array that isn't empty
if (is_array($source) && !empty($source)) {
// Loop through the array and start setting stuff
foreach ($source as $k => $v) {
/**
* This is where the setting magic takes place, putting the
* hierarchy together for the entire config tree.
*/
$newkey = $key === null ? $k : "$key:$k";
// Set the new values into the config
self::$_config[$newkey] = $v;
// Run through this process again until we need not do this
self::_setConfig($v, $newkey);
}
}
}
/**
* Sets the raw config array data for use later, if it is needed
*
* @access public
* @param array $config Config array to set into the raw array
*/
protected static function _setRaw($config) {
if (is_array($config) || is_object($config)) {
self::$_configRaw = array_merge(self::$_configRaw, (array) $config);
}
}
}
You might notice that the method Padlock_Config::setup() is where everything takes place. Because this is essentially a deciding method I have included the Padlock_Config_Array class so you can see what happens when you load an array into the Padock_Config class for setting.
<?php
/**
* Padlock - The PHP Application Developer's Library of Objects and Code Kits
*
* @category Padlock
* @package Padlock_Config
* @author Robert Gonzalez <robert@everah.com>
* @license PLEASE SEE ACCOMPANYING LICENSE TEXT OR THE {@see Padlock::license()} method
* @version $Id: Array.php 32 2008-08-20 00:59:12Z robert $
*/
/**
* Padlock Configuration Array handler class
*
* The Padlock Configuration Array handling class handles setting of config params
* from an array. The nature of this handler builds the configuration array into
* tiers that are made up of other segments of the initial data set so that the
* hierarchy of config elements is easily fetched.
*
* @author Robert Gonzalez <robert@everah.com>
* @category Padlock
* @package Padlock_Config
* @version @package_version@
*/
class Padlock_Config_Array extends Padlock_Config {
/**
* Sets a series of properties from an array of properties
*
* @access public
* @param array|object $config Array or object to set values from
*/
public static function setFrom($config) {
self::_setRaw($config);
}
}
Please keep in mind a few things about this: 1) I have an autoload method registered in the Padlock superclass that handles loading of support files, so you won’t see includes and requires anywhere in here, and 2) The file class has considerably more code than the array class.
To use this, all you do is create your array and pass that to the Padlock_Config::setup() method. After that you can use Padlock_Config::get('path:to:a:nested:config') to get to one, or use Padlock_Config::fetch() to get all of them in the config class’ core config array. For reference, that array would now look like:
array(21) {
["defaults"]=>
array(2) {
["page"]=>
string(5) "index"
["action"]=>
string(7) "default"
}
["defaults:page"]=>
string(5) "index"
["defaults:action"]=>
string(7) "default"
["routes"]=>
array(3) {
[0]=>
string(10) "controller"
[1]=>
string(4) "view"
[2]=>
string(5) "model"
}
["routes:0"]=>
string(10) "controller"
["routes:1"]=>
string(4) "view"
["routes:2"]=>
string(5) "model"
["application"]=>
string(5) "Bilbo"
["nested"]=>
array(3) {
["levels"]=>
array(3) {
[1]=>
string(5) "floor"
[2]=>
string(6) "garage"
[3]=>
string(6) "canopy"
}
["names"]=>
array(3) {
[1]=>
string(6) "Hectar"
[2]=>
string(5) "Jonas"
[3]=>
string(4) "Phil"
}
["named"]=>
array(3) {
["floor"]=>
string(6) "Hectar"
["garage"]=>
string(5) "Jonas"
["canopy"]=>
string(4) "Phil"
}
}
["nested:levels"]=>
array(3) {
[1]=>
string(5) "floor"
[2]=>
string(6) "garage"
[3]=>
string(6) "canopy"
}
["nested:levels:1"]=>
string(5) "floor"
["nested:levels:2"]=>
string(6) "garage"
["nested:levels:3"]=>
string(6) "canopy"
["nested:names"]=>
array(3) {
[1]=>
string(6) "Hectar"
[2]=>
string(5) "Jonas"
[3]=>
string(4) "Phil"
}
["nested:names:1"]=>
string(6) "Hectar"
["nested:names:2"]=>
string(5) "Jonas"
["nested:names:3"]=>
string(4) "Phil"
["nested:named"]=>
array(3) {
["floor"]=>
string(6) "Hectar"
["garage"]=>
string(5) "Jonas"
["canopy"]=>
string(4) "Phil"
}
["nested:named:floor"]=>
string(6) "Hectar"
["nested:named:garage"]=>
string(5) "Jonas"
["nested:named:canopy"]=>
string(4) "Phil"
}
I hope this is useful for someone in some capacity. I played with this for a little while before getting it right. But now it is something that I am using all over the place.
Good luck with your coding and happy PHPing (did that sound weird or what?)!
Today I had the misfortune of having to migrate a website from one server to another for a sister company of ours. I say misfortune because this website is coded in the nastiest bit of Cold Fusion code I have ever seen. I mean it looked like a script kiddie that just learned how to regurgitate CFML from the back of a cereal box tutorial wrote it. And I had to make it work on a new server.
Did I mention that I haven’t been in Cold Fusion for over five years? And I hated it then.
Good thing that I am a fairly adept PHP developer. Instead of futzing with crappy CFML I decided to spend a few hours trying to port it to PHP in a sensible way with sensible coding and sensible architecture. The result was that I built a mini-MVC framework for it in about 4 hours. Aren’t I awesome? Well, isn’t PHP awesome?
I can do a lot of things in PHP typically in a short amount of time. I can write test snippets, mini-apps, middleware, all sorts of goodies fairly quickly when I need to with this language. And what I write generally is easily understood (comments anyone?) and easily maintained. I cannot say that about that pile of crap that was the Cold Fusion site. That stuff stunk like butts on a hog. But I shall try to not focus on that so much as tell you how much I love PHP today because it let me create a nifty little mock MVC style framework in about half a day.
Well, actually it is more of a VC framework with a small data handler object that talks to MySQL only. But it gets done what is necessary in an OOP style and is very easy to understand and maintain. Which is what I want.
Now all I have to do is port their templates and CSS over to something cleaner and I will be able to get their site off that vomitous mass of Cold Fusion Crap and on to something a lot prettier, easier to manage and, in my opinion, just plain better.
PS And if you are ever going to make a static HTML page for a single row of a database table, do not, anywhere in your code, set the row ID of the page information as it comes from the database as a hard coded numeric valued variable, then check if that variable is numeric, then pass that to the database as a query to get the page information of the page you are on. That is just stupid, plain and simple. Be smart, develop smart and use your brains. That is why we have them, to keep from making stupid mistakes that other, smarter, people might laugh at you for.
This past Monday I had the fortune of attending my very first meetup. I had set a goal for myself a few weeks ago that I would attend one before the end of the year. It happened way sooner that I thought it would and resulted in me being able to mix it up with a few fine folks at the Silicon Valley MySQL Meetup.
There was a business need for me hitting this thing up. Firstly, I am a web developer. I am not a DBA and I am certainly not a MySQL guru. I can write queries but the extent of my MySQL knowledge ends right about there. Secondly, I am responsible for management and maintenance of all of our MySQL servers at work. There is one chief DBA who is a Sybase queen, another two or three folks that know their way around a Sybase server (and to a lesser extent a general database server) and then there is me and my colleague who write web apps and MySQL queries. Thirdly, I am an administrator for a very popular PHP developers forum and knowing how to get myself out of the stupid ass scraped I have gotten us into when it comes to MySQL would be darn handy.
So I set out to hit this meetup. It was held at the Sun Microsystems complex in Palo Alto (or Redwood City or wherever you are when you come off the Dumbarton Bridge on the Peninsula side). I was looking forward to finally meeting a man that I have been communicating with for some time now (yes, you Don) and to networking with other MySQL/PHP/Web developers in my local geographic region. And I was not disappointed.
The talk itself was a little boring to be honest. But that is because I am not at all interested in database shards and the whatnot. However I was very interested in meeting Don Ravey, a fellow moderator on our forums, Mr. Tish Wood, a very prominent member of the PHP Meetup community and one of the coordinators of the meetup Eric Bergen from Proven Scaling. Not only that, but there were a host of other people there that provided excellent commentary, questions and challenges for the speaker of the evening.
I love being in the mixed company of brilliant people like that. It is humbling, exciting and gives me something to look forward to. I so enjoyed being there, connecting, talking with people, meeting a few folks and hopefully being able to glean something from someone somewhere. I just hope that I can, at some point, make it to another meetup. Perhaps a LAMP meetup or a PHP meetup. After this last meetup Tish asked me if I would at all be interested in speaking to a group at the Greater SF Bay Area LAMP meetup. I think I would love to do that.
If I can find an evening to get away for a while longer than this last evening. And if we can find a way to not have a meetup in San Francisco. Because as much as I like socializing and hanging with other technology professionals, I cannot fathom the thought of heading to The City for a 7:00 PM meeting on a weekday evening. Until then though, I can start thinking about what I would talk about if I were to ever talk.
And hopefully I will be a little more relevant to a n00B than that fella at the MySQL meetup was to me. He was good and he knew his stuff for the most part. But it was not my cup of tea so I sort of lost interest a little in the subject matter. That was ok though, because I met people, got numbers and had a free coke. In the end, what could be better than that?
Today a friend of mine asked me if I knew of a way to find the next 12 months (starting with next month) and returning an array of those months and their corresponding year. I told him that I was certain that I could do something like this and set out to do it. I came up with:
<?php
/**
* Gets the next 12 months of the year, starting again at January.
*
* This function will return an array of 'month year' for the next twelve months
*
* @access public
* @return array Array of month and year strings
*/
function getNextMonths() {
/**
* Get this month by number
*
* Using the numeric value of the month allows for easy transposition into a
* new array.
*/
$tm = date('n');
/**
* Get this year - because we will need this for the string output
*/
$ty = date('Y');
/**
* Array of months keyed at 1
*
* These are keyed at 1 because the date() function does this
*/
$ms = array(1 => 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December');
/**
* Create a variable to hold out output array
*/
$ma = array();
/**
* Now build the array, using only one loop
*/
for ($j = 1, $i = $tm; $j <= 12; $i++, $j++, $ma[$j] = $i <= 12 ? "$ms[$i] $ty" : $ms[$i-12].' '.($ty+1));
/**
* Send back what we just made
*/
return $ma;
}
/**
* Output control - for the record, I do not count this as a line of code :-)
*/
header('Content-Type: text/plain');
/**
* Test it
*/
var_dump(getNextMonths());
?>
Thinking that I had found the promise land with the awesome little bit of my brainy goodness I posed this problem to my coworker as a code challenge (we do that from time to time to keep our brains thinking). To my surprise and glee, my coworker came through and kick my sorry ass all over the place with his little piece of goodness (I added the comments):
<?php
/**
* Gets the next 12 months from this month
*
* @return array Array of "month year" strings
*/
function getNextMonths() {
for ($i = 1; $i < 13; $i++) {
$array[] = date('F Y', mktime(0, 0, 0, date('m') + $i, 1, date('y')));
}
return $array;
}
// Test it
var_dump(getNextMonths());
?>
Arrogance is of the devil and I think I have taken my fair share of it. Congrats Jason, you put me in my place. And Mark is now a little happier too because you totally gave him some kick ass code.
Now to open the discussion a tad… which one do you like and why? Comment away. I would be interested to see your comments.
Every now and again someone writes a line or two of code that really makes me smile. Such is the case with this outcome determinating and decision making class below. This code is being reproduced, with or without consent, from the PHP Developers Network’s own scottayy.
<?php
/**
* Coin flipper class helps determine outcome of situations in which an outcome
* cannot be decided by sheer manpower alone.
*
* @author Scott Martin <scottayy@devnetwork.net>
* @license None, don't even try to use this or your hair will turn yellow
*/
class coin {
/**
* The outcome determinators
*
* Each invocation of this object will require a determinator upon which the
* object relies to build a determined outcome. These are those determinators.
*
* @access private
* @var array
*/
private static $_sides = array('heads', 'tails');
/**
* The determinating method
*
* This method, when called, invokes a determination sequence and returns a
* determined value for use in decision making.
*
* @access public
* @return string Randomly selected determinator
*/
public static function flip() {
// Quick, randomize me some determinators
shuffle(self::$_sides);
// Quick, offer it back before it gets angry
return self::$_sides[mt_rand(0, 1)];
}
}
/**
* We should always test our determinating decision establisher
*
* 2,4,6,8 You know you want to determinate
*/
echo coin::flip();
?>
For those that just have to have an object to instantiate (and you know who you are), there is this lightly modified version for your obsessive/compulsive selves:
<?php
/**
* Coin flipper class helps determine outcome of situations in which an outcome
* cannot be decided by shear manpower alone.
*
* @author Scott Martin <scottayy@devnetwork.net>
* @license None, don't even try to use this or your hair will turn yellow
*/
class coin {
/**
* The outcome determinators
*
* Each invocation of this object will require a determinator upon which the
* object relies to build a determined outcome. These are those determinators.
*
* @access private
* @var array
*/
private $_sides = array('heads', 'tails');
/**
* The determinating method
*
* This method, when called, invokes a determination sequence and returns a
* determined value for use in decision making.
*
* @access public
* @return string Randomly selected determinator
*/
public function flip() {
// Quick, randomize me some determinators
shuffle($this->_sides);
// Quick, offer it back before it gets angry
return $this->_sides[mt_rand(0, 1)];
}
}
/**
* We should always test our determinating decision establisher
*/
$coin = new coin;
/**
* 2,4,6,8 You know you want to determinate
*/
echo $coin->flip();
?>
See, just looking at that code makes you want to smile doesn’t it? Geeks are great.
A recent Sitepoint article by Rachel Andrews, Director of edgeofmyseat.com, outlined some pretty Nifty Navigation Tricks Using CSS. Anyone that wants to learn to make some pretty cool tabbed, button or vertical bar navigation lists should give this article a read through. It is a pretty well written article and has a great deal of code that can be easily copied and pasted for your development pleasure.
For four pages it reads very fast. It is easy to follow and the examples are practical. I wish there were some working samples of the code, but still, it is a good teaching tool for those that have yet to dive into CSS based navigation lists (and you should get into it, as CSS is designed for such things).
For those that want a huge assortment of samples, code and really cool lessons, check out Stu Nicholls’ CSS Play. This site is an amazing reference for learning the art of cross-browser compatible CSS. There are menus, layouts and much more available to learn from and even use, in many cases without even a link back to him, though it is always a good idea to give credit where credit is due.
So if you are in need of a little CSS learning fix, hit these references up. You will enjoy them and, in the case of Stu Nicholls, may even make them part of your normal daily web development toolset.
A few weeks ago I was toying with XML. I had a few minutes to spend teaching myself so I decided that I would use that to work on something that I desperately need to work on. XML.
And what better way to learn a little XML stuff that on the Amber Alert system? So I began my journey.
Continue reading »
I was thinking today that though I carry the title of Web Developer around with me wherever I go I have not been developing for the web much the last few months. Come to think of it, I cannot say that I have developed anything for the web in about five months or so.
Don’t get me wrong. I have been coding PHP like a mad penguin with 20 hands and an incredible itch at the tip of his fingers. Its just that the code I have been writing has been either framework code that drives web apps or CLI code that will be used for integration projects from the command line or cron.
And I absolutely love what I am doing. I have been employing design patters almost as though they came straight from a book. Without even knowing I was doing that.
And it made perfect sense to do it. Little modular classes that are built of maybe two methods and a property each that totally allow for expansion later just by adding the controller and the model. You just gotta love it.
And because I have had the opportunity to do so, I have been using vi(m) quite a bit more than I have in the past. In fact, today I used nothing else. And I was totally thrilled to use it.
I felt faster and way more in control without having to touch my mouse. And I was able to code, shell out, process SVN commits, file searches, CLI stuff… everything I needed to do without ever leaving my editor and without ever touching my mouse. How awesome is that?
I think my zeal for the type of coding is that I felt like a programmer today. Not a script kiddie, not a PHP n00b, but a programmer. A guy who writes code then implements it and instantiates it at the CLI to watch (you ready for this?) absolutely nothing come to the screen - because that is how we want it to work.
Yeah, I love being a geek. And it doesn’t take much to make me happy as a geek. A sweet little editor, a freaking rad OS and a great programming language like PHP. Oh yes, color me happy. And color me geek.
According to an article today by TechCrunch, Zend Technologies, The PHP company, is cutting 25% of their PHP developer staff, perhaps with an eye towards selling the company.
Israeli startup Zend Technologies has fired 25 percent of its R&D team (at least ten people), as well as others across the company, in an attempt to become cash flow positive, says a source close to the company. A spokesperson from the company’s PR firm says: “Yes, I can confirm that Zend made the layoffs, but we cannot comment on the numbers or reasons for the action.”
Read the complete TechCrunch article here.
As I read the brief article I began to think that this is not really anything that should be too newsworthy. Many companies in the USA, and around the world in general, are feeling the pinch of a down economy. Companies have to do what they can to reduce cost while maintaining competitive prices for goods and services. What Zend is doing is not really that out of the ordinary.
And I am not sure that Zend, as a company, is really worth a huge amount of money like the Sun Microsystems acquisition of MySQL was. It might be tasty to some of the players in the industry right now like Oracle, IBM or even a Sun. But really, other than the Zend engine and the Zend IDE what exactly does Zend have to offer?
Whatever happens to Zend from all of this one thing is very important to remember. Many a PHP developer now has the opportunity to seek gainful employment from other companies that are seeking, heavily, PHP talent. Many Silicon Valley companies, including many companies in the social networking space, are looking for top tier PHP talent right now. Companies like Ning, Digg, Facebook, Technorati, Yahoo and Google are constantly hiring PHP developers.
Times are good for being a PHP developer. Maybe not so much if you worked for Zend. Nonetheless, now is a great time to know a great deal about PHP.
I just recently moved from one server to another and am testing out whether the blog conversion worked or not.
If it worked then this post should appear without issue. If it didn’t, well then you probably would not even see this post at all.
So here’s to keeping our fingers crossed.