Category

Booked

Look Mom, No Hands! Test Driving Code Without A Mocking Framework

I love TDD. I haven’t found a more effective way to incrementally design and build software in the 15+ years that I’ve been doing this. I have formed and evolved a lot of opinions about how I approach TDD, though.

Recently, I wrote a post for EuroStar Software Testing titled Look Mom, No Hands! Test Driving Code Without A Mocking Framework

This is a topic that has been on my mind for a long time. It’s not intended to start a mocks vs stubs flamewar or anything like that. Instead, I wanted to walk through my progression of TDD practices over the years and share what I’ve learned.

Don’t get me wrong – test-driving with a mocking framework is better than not test-driving at all. I just prefer stubs.

Looking back at the test cases in the Booked source code which utilize PHPUnit’s mocking framework (yes, there are still a lot), I can see just how entangled the test code is with the implementation of the production code. The source for Booked changes frequently and it is covered by more than 1000 unit tests. New features are introduced and, occasionally, some of the unrelated tests fail.

They fail because there is too much specified in the mock setup. In order to validate the behavior of some area of the code, I have to set up unrelated mock expectations to get collaborating objects to return usable data. If I change the implementation of an object to no longer use that data, my test shouldn’t fail.

A couple of years ago I stopped using PHPUnit’s mock objects and I’ve seen the resiliency of my unit test suite increase. I’ve also seen my development speed and design quality improve. Instead of ambiguous mock expectations scattered throughout the tests, I’ve built up a library of stub objects which have logical default behavior.

When test-driving increments of functionality, I’m able to concentrate on the behavior that I need to implement rather than getting distracted with test setup and management.

More focus. Better design. Higher quality. No mocks.

Booked 2.7 is out in beta!

I’m thrilled to announce that Booked 2.7 has been released in beta. This version is packed full of long-requested features.

I don’t care about the details – how do I get it?

Eager to try it out? I don’t blame you 🙂

Download the beta from SourceForge

Give feedback and ask questions

Try the live beta demo

This is still beta software. While I’ve tested this pretty well, I wouldn’t recommend production usage just yet!

So, what’s all this new stuff?

I’m glad you asked!

Charging for Reservations

One of the most requested features over the years what the ability to hook into payment gateways to charge for reservations. 2.7 comes with integrated support for Stripe and PayPal.

You can now let your users purchase credits to be used for reservations. This harnesses the power of the Booked credit system – allowing admins to set peak and standard usage, configuring different credit rates for different resources – and seamlessly integrates it with payments. Your users will see an upfront cost for a reservation along with their credit balance. (For security reasons, this functionality is not enabled in the demo.)

Set up your payment details by setting allow.purchase to true in the credits section of your application configuration. Then open up Application Management > Payments to set the cost per credit, view the transaction log, and configure payment gateway details.

Terms of Service

Gaining a user’s consent before allowing access to a resource is a critical part of the reservation workflow for many organizations. You can now upload terms, link to terms, or simply embed them directly into the application. You can configure when to prompt users for acknowledgment of the TOS, either for each reservation or during registration.

Add terms of service from Application Management > Reservations, then choosing Terms of Service from the right hand drop down.

Schedule Availabiity

A schedule may only be available for a portion of the year. Seasonality, staffing, or business needs may allow reservations for a limited period of time. Per schedule, admins can now configure an option open and close date for a schedule. So if you have an event that runs May – July, it’s simple to ensure only the available dates are shown and limit reservations to that date range.

Set schedule availability from Application Management > Schedules, then editing the Available dates.

Overlapping Resource Reservations

A fundamental usage for Booked is to help organizations ensure that resources are never double booked. But many of you have asked to bend that rule. It’s now possible to let all resources on a schedule be booked by more than one user concurrently. Schedules set up to allow concurrent reservations will flip to the calendar view for a simple display of all activity.

Allow overlapping reservations from Application Management > Schedules, then changing the option to allow resources to be reserved by more than one person at a time.

Fully Customizable Layouts

In most cases resource availability follows a fairly standard schedule – every 30 minutes between 9am and 5pm, for example. But there are some cases where you want to create very specific slots and prevent bookings at all other times. Say you want to set availability for two slots per month – an 8 hour slot on the first and second Friday. Switching your schedule to use a custom layout gives you full control to set specific availability times. These slots will show up as available on the calendar view and the schedule view will only show these times.

Customize your layout from Application Management > Schedules, then clicking Switch to a custom layout.

Distinct Add, Update, Cancel Notice Times

A simply powerful feature of Booked is the ability to set how much notice must be given before a resource can be reserved. But until now the notice period applied to adding, updating, and cancelling a reservation. An admin can now configure a resource to require 24 hours notice for reservation, 2 hours notice for an update, and 48 hours notice for cancellation, for example.

Set distinct notice times from Application Management > Resources, then setting any of the made, updated and deleted settings under the Access section for a resource.

Announcements on the Login Screen

Important announcements and updates can apply to all users. When posting announcements, admins can now choose where it shows up. The same power you have to set display times and priorities works for login announcements, too!

Add a login announcement from Application Management > Announcements, then choosing Login as the display page when creating it.

Default Group Membership

Would you like all new accounts to be added to groups by default? Finally, it’s possible by simply setting the group as a default.

Set default groups from Application Management > Groups. Check Automatically add new users to this group when adding or updating groups.

Multiple Resource Images

Another long-requested feature finally makes it to prime time. There’s not much to explain here – you can now upload an unlimited number of resource images. If there is more than one image for a resource, we’ll show a carousel and let your users scroll through all the pictures.

Add multiple resource images from Application Management > Resources, then changing the resource image. You can add as many images as you want!

Embed a Booked Calendar Directly in Another Website

Until now you’ve had to either IFRAME (ech!) or use the API to display a Booked calendar on another website. We now have the ability to include a single script reference to load a configurable view of Booked events. Just enable public visibility for a resource or schedule, then add drop one line of HTML on a page!

And more

There are dozens of other enhancements and fixes in 2.7 to make Booked the best resource scheduling software you’ll ever use!

  • Added ability to set comma or semicolon delimited admin.email configuration setting to allow multiple admin emails
  • Added ability to send a reservation to Google Calendar
  • Added ability to select a resource image while adding
  • Added ability to begin a reservation directly from Slack
  • Added ability to set view-only resource permissions
  • Added ability to sync group membership from LDAP and CAS
  • Added blackouts to schedule and resource calendar view
  • Added view calendar page
  • Added ability to require reservation title and description
  • Added user groups to report output
  • Resource QR code will open ongoing reservation if it requires check in
  • Upgraded jQuery to latest
  • Bugfixes

A Shout Out for Hosting

Love Booked, but hate the idea of installing, managing, and supporting yet another application? We offer professional Booked hosting directly from the authors. For just $10/month you get unlimited usage of Booked, premium support, early access to features, and more.

Start a no-obligation 30 day free trial now!

Booked Feedback

One of the things I love most about working on open source software is the engagement of the community. Booked has a very active community with a huge repository of information.

An increasingly common problem I’m hearing is that Booked has a steep learning curve. Being in the application every day, I have blinders on that hide these types of issues from me.

How can we clean up the workflow and make Booked a pleasure to use? I want to know what’s difficult. I want to hear your thoughts on how to improve the initial application experience.

My goal is to get to a point where new users do not need a help page or a tutorial. I want you to go from install to booking a reservation in minutes without needing help from the community.

Submit your feature requests and improvement suggestions!

I can’t promise that every suggestion will be implemented, but I can promise to consider each suggestion and use your feedback to improve Booked.

Twinkle Toes Software – Looking back on my first year

Wow, it’s been a year since I founded Twinkle Toes Software. I thought this would be a good time for me to stop and reflect a bit on how its going and what I’ve learned.

The Zero Entry Pool Approach

Am I a “zero-entry” entrepreneur? I’m sure there are countless ways to approach starting a business. For me, I decided against diving head first into it and instead opted for what I’m calling a “zero-entry” approach.

Have you been in a zero-entry pool?

Zero Entry Pool

This has a gradually declining entrance that allows you to enter the pool at your own pace. Compare this to a diving board, where you have very little control over the speed at which you enter – and there’s no turning back. I’ve taken the former approach to launching Twinkle Toes Software. At this point I’m about waist-deep 🙂 And this is what I’ve learned.

So… How’s It Going?

Thanks for asking! It’s going well. Interest in Booked Hosting is strong, as is the interest in custom development. Surprisingly, there hasn’t been much interest in our enterprise support services. Our community support is incredible, so I guess I shouldn’t be all that bewildered.

What I’ve Learned – Get Your Ducks In a Row

If you’re going to start a business, whether it’s a sole proprietorship (like Twinkle Toes), an LLC, S Corp, whatever, get those ducks in a row.

Ducks in a row

The Legal Stuff

First of all, I’m not a lawyer and this isn’t legal advice, so consult an attorney for all legal matters. Every state and business type has their own set of rules. Find out what yours are and follow them. Better yet, hire an attorney to help you or use a site like LegalZoom (I don’t use LegalZoom and have no affiliation with them). There are different advantages and rules that go into different business registration types. Find the one that makes sense for you.

The Small Business Association here in the US has a lot of great advice and tools for launching your business. Definitely check that out.

Get your company registered with your state, city and county municipalities. Get an EIN and keep your social security number out of all records. Keep your personal and professional identities separate – your business is one thing and your personal life is another. The more you keep these isolated from one another, the better.

Make sure you have the right insurance and make sure you have enough of it. If you keep your business legally separate it can be difficult to jeopardize your personal finances in the event that legal action is taken against you. That said, you want to ensure you have insurance that offers the right amount of protection for your situation.

Set Up a Professional Web Presence

If you have a website that looks like it was built by a kid you will miss out on business. Really, would you give your money to a business that does not present themselves in a professional way?

Unless you’re a web designer, don’t waste your time building your own web site. Joomla and WordPress are readily available and there are thousands of themes available, most at a nominal cost. Set it up and spend your time focusing on content and marketing rather than design.

Ongoing Operations

Keep Track of Every Dollar Coming In and Going Out

Keep business expenses and income separate from your personal accounts. Set up a separate bank account if you want to be safe. Taxes are not fun. Taxes are even less fun when you’re reporting for your business, too. Hire an accountant if you are pulling in enough money to make it worth the cost. Otherwise, use an accounting package. I’ve heard good things about QuickBooks, but I’ve never used the software.

I do use Wave and would highly recommend them. The software is web-based, so it is available anywhere. This is great when you’re not in front of your primary computer or need to capture transactions on the go. They provide quoting, invoicing, accounting, credit card payment processing and more. Amazingly, it’s also free (aside from the credit card processing, which runs at a reasonable 3%).

Different business types have different accounting rules. Know what those rules are and follow them. If you’re going all in and will be self-employed make sure you know when and how to pay your taxes. Actually, just hire an accountant. My guess is that you’re not a tax or finance expert so find someone that is.

Analyze Your Web Traffic

The more you understand your website’s audience, the more effectively you can tune your website. Google Analytics makes it trivial to capture and analyze your web traffic. See where people are coming from, their navigation path, how long they stick around, etc. This is crucial information to help keep an eye on the effectiveness of any marketing campaigns or call to action sections of your website.

It’s Hard Work

Take That Estimate and Double It

I don’t know anyone who started their own business thinking it would be easy. I believed it would be a lot of work, but I was wrong. I underestimated how much work it is. Be prepared to be working long days. Set these expectations with your family. You absolutely need their support if you’re going to be successful.

Also, set realistic timeline expectations with clients. Don’t promise to turn something around in a few days when you know you need weeks with your current workload. You can attract a lot of clients by promising quick turnarounds, but you’re going to lose them and your reputation will be tarnished if you can’t keep your promises. In my experience most clients want things delivered immediately, but understand and accept longer estimates. Be honest and deliver and you’ll build a loyal client base.

You Will Probably Lose Money

There are very few business that launch and immediately turn a profit. Be prepared to lose money for months, maybe even years. You may never make money. Something like half of all new businesses fail in the first few years.

What does this mean?

If you’re quitting your full-time gig for this then have enough in your savings to live off of for at least a year. If you’re doing this part-time then be ready to invest a lot of your free time and money. Find investors if necessary.

Booked was in the marketplace for 10 years and had a reputation as a successful open source product. Even with that history, I’m barely turning a profit. I lost money during the first 6-7 months after the Twinkle Toes launch.

But It’s Fun

I’m having a fantastic time running Twinkle Toes. Yes, it’s a lot of work but it’s work that I am passionate about. Booked Scheduler is very close to my heart and being able to dedicate time to it while making a little bit of money feels great.

What Did I Miss?

Anyone have any other tips? Let’s hear about your experiences launching a business or product!

Booked 2.5.9 release and some details about 2.6

Booked 2.5.9 was released earlier this week. I’ll wait while you go grab it. If you subscribe to our hosting service, you’re already updated.

As far as minor releases go, this was a biggie. Some frequently requested features finally made it in, like showing custom attributes on reports, more options for what to show in the reservation label, reservation approval request emails and much more.

It took a while to get this release out the door. Partly because there were a lot of features added, but also because I’ve been spending a lot of time on custom development. The good news is that most of the custom work will be making it’s way into the main code base.

Which leads me to the state of Booked version 2.6. This version has been in the works for a while now and it will be a significant update. It will also very likely be the last major release before Booked 3.0.

So what will be in 2.6, you ask?

A lot.

The biggest feature is a new UI which will be mobile friendly. Booked “works” right now on mobile, but it doesn’t work great. Mobile will be a first class design concern in 2.6.

Custom attributes will also be getting some love in 2.6 with support for admin-only attributes, the ability to prompt for reservation-level attributes only when specific resources are booked, and setting reservation colors based on custom attribute values.

Some other features that are in 2.6 include the ability to tie accessories to resources, ability for guests to participate in reservations, the ability for guests to book.

The plan also includes support for some alternate views of a schedule, such as for a monitor or a small tablet outside a conference room. Checking in and out of reservations and automatically releasing reservations are on tap. I’m also planning to support both Facebook and Google sign in.

As for a release date, I’m not yet sure. With my current pace I’d expect to have a beta out in the spring of 2015. If I get some development help the timeline may shrink considerably.

The full list of feature requests can be found here and I’ll be keeping this up to date as 2.6 progresses. Please keep the feature requests coming! That’s how Booked became great and how it will continue to be great.

Update November 26th

You’ve got to love open source software! A defect was found in 2.5.9 that prevented emails from being sent on *nix machines. I released 2.5.10 last night.

We’re Officially Booked!

A New Name

booked

It’s been many months since the last phpScheduleIt release and a lot has happened since then. The biggest change is that I decided to rename the project to Booked. I’ve never been completely happy with the previous name and received a lot of feedback about it. I landed on Booked because it succinctly gives you and idea of what the product is and it’s easy to remember.

A New Company

TTS17

The next big change is the formation of Twinkle Toes Software. Words typed here on this blog cannot express how excited I am to finally make this announcement. I’m an entrepreneur at heart and have wanted to start my own business for a while. Booked gives me an incredible opportunity to bring that dream to life. We’ll be offering professional services such as support, custom development, and hosting. We’re here to stand behind Booked and help it grow. I also have to thank my good friend Mandy for helping me with the website design and logo.

There is plenty more to come including the launch of booked website, the announcement of our hosting partner and some great new features and enhancements to the Booked community.

New Features

Oh, and I forgot to mention that there are a ton of great new features with this release! Here’s what you’re going to get with Booked 2.5. You can download it here

  • Application renamed from phpScheduleIt to Booked Scheduler (why?)
  • Added ability to reserve resource groups
  • Added ability to filter schedule resources
  • Added ability to specify resource type
  • Added enhanced resource status management
  • Added ability to specify buffer time between reservations (per resource)
  • Custom attributes now appear on all reservation emails and balloons
  • Added ability set custom attributes for an individual resource, user or resource type
  • Added ability manage config files for all plugins through the UI
  • Added ability to set reservation colors per user
  • Added ability to subscribe to reservation Atom feeds
  • Added ability update blackouts
  • Added ability attach multiple items to a reservation
  • Added Shibboleth authentication plugin (thank you to the folks at UCSF)
  • Added ability to email admin for all new account creations
  • Updates and cleanup on the API
  • Removed password regex setting in favor of password complexity settings
  • Changed schedule drop downs to exclude schedules if the user does not have permission to any of the resources belonging to it
  • Added wide and condensed booking page views
  • Added option to allow all users access to reports
  • Added setting for default ‘from’ email address
  • Changed the reservation page to default to the minimum resource reservation time
  • Changed reservation update to grant permissions to all users if auto-assign permissions is being turned on
  • Fixed showing ‘Private’ when the current user is the reservation owner
  • Fixed bug where recurring reservations across daylight savings time boundaries were not being updated to the correct time
  • Fixed bug where schedule would freeze on certain daylight savings boundaries
  • Fixed pagination bug on manage reservations page
  • Fixed bug allowing invitees to join a reservation that was already at capacity
  • Fixed bug not enforcing resource cross day reservation constraint
  • Fixed bug where quota rules were being enforced cumulatively for resources on a schedule
  • Fixed bug where reminders were being sent for deleted reservations
  • Updated all mysql_* calls to mysqli_*
  • Numerous other minor fixes and updates

Extending phpScheduleIt : Getting started with the API

One of the big features we introduced in 2.4 was a RESTful API for integrating with phpScheduleIt. This is disabled by default but can be flipped on just by changing the config setting $conf['settings']['api']['enabled'] to 'true'. Once you do that, navigate to the auto-generated documentation page at http://your_phpscheduleit_root/Web/Services/index.php (if you’ve got mod_rewrite set up with Apache, you won’t need ‘index.php’) and take a look at everything that’s available.

Each service description contains some important information. You’ll see if the method is invoked via GET, POST or DELETE. You’ll also see:

Name  – This is the name of the service endpoint and the URL segment to use.
Description – A brief description of what the service does and any optional parameters.
Route – The full route and variables needed to invoke this endpoint. Any portion that begins with a : denotes a variable that must be provided. Pay attention to any trailing slashes – those are required when specified.
Response – An example JSON response (if applicable).
Request – An example JSON request (if applicable).

You’ll notice that all requests/responses are in JSON. In this post we’ll use jQuery to authenticate, list our reservations, and update a reservation. It’s important to remember that you cannot POST JSON data cross domain with JavaScript. So if you want to use this method you’ll need to run it from the same domain as your phpScheduleIt instance.

Authenticating

Just like the web front end, most of the services that the API exposes require an authenticated session. The documentation will note whether or not a service is secure or not. So step one will be to get a session token to use for subsequent calls.

Here’s our authentication fragment. It’s pretty simple, really. We POST the username and password to the Authentication/Authenticate service, then store the sessionToken and userId that we get back. We’ll just pop up and alert if the authenticate call fails.

 $.ajax(
{
	type: "POST",
	url: self.baseUrl + "Authentication/Authenticate",
	data: JSON.stringify({username: self.username(), password: self.password()}),
	dataType: "json"
})
.done(function (data)
{
	if (data.isAuthenticated)
	{
		self.token(data.sessionToken);
		self.userId(data.userId);
		self.headers = {"X-phpScheduleIt-SessionToken": data.sessionToken, "X-phpScheduleIt-UserId": data.userId}
		self.authenticated(true);
	}
	else
	{
		alert(data.message);
	}
});

Finding Our Reservations

Now that we have a session established we have all the rights and privileges of that user. We’ll start off by getting a list of our reservations. By default, this will get all of the reservations for the authenticated user in the next two weeks. This is a simple GET request to Reservations/. The important part is that we attach the headers to this request.

$.ajax(
{
	type: "GET",
	url: parent.baseUrl + "Reservations/",
	headers: parent.headers,
	dataType: "json"
})
.done(function (data)
{
	self.reservations.removeAll();

	$.each(data.reservations, function (idx, val)
	{
		self.reservations.push(new ReservationItemViewModel(val, parent));
	});
});

Updating A Reservation

Alright, that was almost too easy so far – let’s get into the nitty gritty. Create and update calls require a full object to be posted. To do that, we’ll first need to load the reservation and map it into a request object. This one is a little more involved, so I’ll walk you through it.

First, we make a GET request to Reservations/:referenceNumber with the reference number of the reservation that we want to update. Next, we walk through the response object and convert it into a request object. Most of this is straightforward. You may be asking why we’re calling $.map for the array objects. This is because the request object needs a different object structure than what what we currently have. So we’re just looping through each one and converting the item into the format needed for the request.

$.ajax(
{
	type: "GET",
	url: parent.baseUrl + "Reservations/" + reservationItem.referenceNumber(),
	headers: parent.headers,
	dataType: "json"
})
.done(function (data)
{
	existingReservation = data;
})
.then(function ()
{
	var request = {		
		accessories: $.map(existingReservation.accessories, function (n)
		{
			return {accessoryId: n.id, quantityRequested: n.quantityReserved };
		}),
		customAttributes: $.map(existingReservation.customAttributes, function (n)
		{
			return {attributeId: n.id, attributeValue: n.value};
		}),
		endDateTime: existingReservation.endDateTime,
		invitees: $.map(existingReservation.invitees, function (n)
		{
			return n.userId;
		}),
		participants: $.map(existingReservation.participants, function (n)
		{
			return n.userId;
		}),
		recurrenceRule: existingReservation.recurrenceRule,
		resourceId: existingReservation.resourceId,
		resources: $.map(existingReservation.resources, function (n)
		{
			return n.id;
		}),

		startDateTime: existingReservation.startDateTime,
		title: existingReservation.title,
		userId: existingReservation.userId,
		startReminder: existingReservation.startReminder,
		endReminder: existingReservation.endReminder
	};
	self.UpdateReservation(request, reservationItem);
}
);

We then make any changes we want (in this case, just updating the description) and issue the update. If we get an error during the update call we’ll display it. The error messages will be the same ones we get when we have a problem saving a reservation from the web. Expect to see messages like missing required fields, invalid dates, or conflicting reservations.

request.description = reservationItem.description();
$.ajax(
{
	type: "POST",
	url: parent.baseUrl + "Reservations/" + reservationItem.referenceNumber(),
	headers: parent.headers,
	dataType: "json",
	data: JSON.stringify(request)
}
)
.done(function (data)
{
	console.log("Update response message: " + data.message);

	self.LoadReservations();

}).fail(function (data)
{
	reservationItem.saveErrors(data.responseJSON.errors);
})

Putting It Together

Here’s the full example. Full Example Code – You’ll need to View Source and save this file locally.

What’s up with phpScheduleIt 2.4?

It’s been nearly 5 months since the 2.3 release of phpScheduleIt and I’m sure some are wondering what’s going on. Well, a lot of time has been spent working with BrickHost in setting up a cloud-based hosting solution for phpScheduleIt. This will be a turn-key solution with full support for a great price.

But development towards a 2.4 release is in progress! The main focus of 2.4 is a comprehensive public API. This will allow developers to easily integrate with phpScheduleIt and paves the way for mobile versions of the application. In addition, here are the other features that will be part of 2.4

GUI based configuration editor. No more need to open up the config file for basic changes.
config

Daily layouts. Full support for different schedule layouts for each day of the week.
daily

WordPress authentication plugin. Single sign-on between phpScheduleIt and WordPress.

I’m also planning to include reservation reminder emails and ReCAPTCHA support. With a little luck and a lot of free time, we should be shipping 2.4 soon.

Stay tuned!

Extending phpScheduleIt – Writing a Pre-Reservation Plugin

My previous post walked through configuring and enabling plugins in phpScheduleIt. That’s great… but what if we need something custom to our environment? The most popular support requests I get are for changing different workflows – especially around the reservation process. Today I’m going to dive into how to write plugins for the pre reservation events. If you know some PHP and a bit of object oriented design, this should be pretty straightforward.

Note: The version of phpScheduleIt at the time of writing is 2.2. While the exact details explained in this post may not apply to future versions, the core concepts will.

Getting Started

phpScheduleIt’s plugin architecture is largely convention-based. This means that a few rules apply to all plugins.

  • The plugin package structure must contain the following:
    • A directory named after the plugin, containing all plugin code.
    • A file within that directory named after the plugin.
    • If there is a configuration file needed, it must be named after the plugin, with ending with the suffix .config.php.
  • The plugin class name must must match the file name (without the .php suffix).
  • Plugins follow the decorator pattern.
    • The plugin must extend or inherit the base class or interface. It must also accept it as the first argument to the constructor (we’ll go into the different classes and interfaces available for each plugin type later).

For example, if we’re writing a PreReservation plugin named PreReservationExample, we would create a directory named PreReservationExample. Within it, we create a file named PreReservationExample.php. This file would contain a single class named PreReservationExample, which would extend the base plugin class and accept an instance of that class as the first constructor argument.

OK, enough with the abstract examples. Let’s get into some code.

Pre-Reservation Classes and Interfaces

The pre-reservation plugin allows you to do all sorts of fun things like custom validations, reservation adjustments, notifications and so on. The base class for PreReservation plugins is a PreReservationFactory. Let’s take a look at the interface.

interface IPreReservationFactory
{
	/**
	 * @param UserSession $userSession
	 * @return IReservationValidationService
	 */
	public function CreatePreAddService(UserSession $userSession);

	/**
	 * @param UserSession $userSession
	 * @return IReservationValidationService
	 */
	public function CreatePreUpdateService(UserSession $userSession);

	/**
	 * @param UserSession $userSession
	 * @return IReservationValidationService
	 */
	public function CreatePreDeleteService(UserSession $userSession);
}

Simple enough, but this gives us the power to hook into events before adding, updating, or deleting a reservation. The only argument to each of these functions is a UserSession, which gives you some insight into who is making each of these calls. Each one of these functions returns an instance of an IReservationValidationService. Let’s look at that interface.

interface IReservationValidationService
{
	/**
	 * @param ReservationSeries|ExistingReservationSeries $series
	 * @return IReservationValidationResult
	 */
	public function Validate($series);
}

Even easier! This has one function: Validate(), which accepts either a ReservationSeries (during add) or an ExistingReservationSeries (during update and delete). This is executed during the add/update/delete event and returns an instance of an IReservationValidationResult. Continuing down the path, we see that this interface is also pretty simple.

interface IReservationValidationResult
{
	/**
	 * @return bool
	 */
	public function CanBeSaved();

	/**
	 * @return array[int]string
	 */
	public function GetErrors();

	/**
	 * @return array[int]string
	 */
	public function GetWarnings(); 
}

All of these classes and interfaces can be found in /lib/Application/Reservation/Validation

Putting It All Together

Let’s say that for add and update we want to enforce some rules based on the value of a custom attribute that we added. For delete, we’re happy with the default behavior. Since we already explained how to create the plugin structure for a plugin named PreReservationExample, let’s stick with that name. So the contents of /plugins/PreReservation/PreReservationExample/PreReservationExample.php would look something like this:

class PreReservationExample extends PreReservationFactory
{
    /**
     * @var PreReservationFactory
     */
    private $factoryToDecorate;

    public function __construct(PreReservationFactory $factoryToDecorate)
    {
        $this->factoryToDecorate = $factoryToDecorate;
    }

    public function CreatePreAddService(UserSession $userSession)
    {
        $base = $this->factoryToDecorate->CreatePreAddService($userSession);
        return new PreReservationExampleValidation($base);
    }

    public function CreatePreUpdateService(UserSession $userSession)
    {
        $base =  $this->factoryToDecorate->CreatePreUpdateService($userSession);
        return new PreReservationExampleValidation($base);
    }

    public function CreatePreDeleteService(UserSession $userSession)
    {
        return $this->factoryToDecorate->CreatePreDeleteService($userSession);
    }
}

Let’s step through this. Our class extends the PreReservationFactory. It also accepts an instance of a PreReservationFactory as the only argument to the constructor with a parameter named $factoryToDecorate. This will be a fully instantiated object, loaded up with all the rules needed during add/update/delete. Our constructor just holds onto that instance as a private field.

Each of the Create* functions are then responsible for returning a service to be used before their respective event. In our case, CreatePreAddService() and CreatePreUpdateService() get the base validation service, then add their validation service onto it. Since we don’t have any custom rules for delete, we simply return the base service.

This is an important point. To extend the base PreReservation behavior, we need to return the base service, or a decorated version of that service. If we want to completely replace the base behavior, simply return your own custom object.

Now comes the fun part, our implementation of CreatePreAddService() decorates the existing instance. We do this by getting the result of the base CreatePreAddService() function, then decorating it with our own validation object. We end up returning an instance of a PreReservationExampleValidation, giving it that base service to wrap. Now, we’ll need to create the PreReservationExampleValidation to handle our custom rule when adding and updating a reservation.

We start off by adding a new file to our plugin directory named PreReservationExampleValidation.php. This contains a single class which must implement IReservationValidationService

class PreReservationExampleValidation implements IReservationValidationService
{
	/**
	 * @var IReservationValidationService
	 */
	private $serviceToDecorate;

	public function __construct(IReservationValidationService $serviceToDecorate)
	{
		$this->serviceToDecorate = $serviceToDecorate;
	}

	/**
	 * @param ReservationSeries|ExistingReservationSeries $series
	 * @return IReservationValidationResult
	 */
	public function Validate($series)
	{
		$result = $this->serviceToDecorate->Validate($series);

		// don't bother validating this rule if others have failed
		if (!$result->CanBeSaved())
		{
			return $result;
		}

		return $this->EvaluateCustomRule($series);
	}

	/**
	 * @param ReservationSeries $series
	 * @return bool
	 */
	private function EvaluateCustomRule($series)
	{
		// make your custom checks here
		$configFile = Configuration::Instance()->File('PreReservationExample');
		$maxValue = $configFile->GetKey('custom.attribute.max.value');
		$customAttributeId = $configFile->GetKey('custom.attribute.id');

		$attributeValue = $series->GetAttributeValue($customAttributeId);

		$isValid = $attributeValue <= $maxValue;

		if ($isValid)
		{
			return new ReservationValidationResult();
		}

		return new ReservationValidationResult(false, "Value of custom attribute cannot be greater than $maxValue");
	}
}

This class is also a decorator, so it accepts a class to decorate as the first parameter to the constructor. Next, lets look at the Validate() method. The first thing we do is call down to our decorated Validate() method. This runs all of the existing PreReservation steps. Then, we check to see if we're in a valid state by checking the value of $result->CanBeSaved(). If everything is OK to this point, then we go ahead and run our custom validation.

Walking through the EvaluateCustomRule() function, we first get our plugin's configuration file, reading settings for the maximum value we allow and the id of the custom attribute we are interested in. Then, we get the current value of that attribute from the ReservationSeries. Finally, if the value is less than or equal to our configured maximum, we simply return a new ReservationValidationResult. This indicates that there are no issues. If the value provided exceeds our maximum, then we return a ReservationValidationResult, passing in false to indicate the failure and an error message to explain what exactly failed. The error message will be displayed to the user.

Configuring Your Plugin

If you wanted to use phpScheduleIt's provided configuration API, you would just need to create and register your config file. This would change the constructor of the PreReservationFactory to something like this:

public function __construct(PreReservationFactory $factoryToDecorate)
{
	$this->factoryToDecorate = $factoryToDecorate;

	require_once(dirname(__FILE__) . '/PreReservationExample.config.php');

	Configuration::Instance()->Register(
				dirname(__FILE__) . '/PreReservationExample.config.php',
				'PreReservationExample');
}

And here is our config file:

$conf['settings']['custom.attribute.max.value'] = '100';
$conf['settings']['custom.attribute.id'] = '3';

We won't go into the details of configuration here, but it's enough to say that configuration files are structured as arrays. All settings are part of the $conf['settings'] array.

Wrapping It Up

As with any plugin, you'll need to tell phpScheduleIt that you want to use it by setting the plugin value in /config/config.php. Our value would be: $conf['settings']['plugins']['PreReservation'] = 'PreReservationExample';.

Hopefully this gives you a taste of what's possible. This is a powerful customization point and there's almost no limit to what you can do here. Also remember that you have all of phpScheudleIt's internal API available to you. Explore it and use it.

You can find the source code for this example in /plugins/PreReservation/PreReservationExample

Happy Coding!

Extending phpScheduleIt: Understanding and Installing Plugins

One thing I really wanted to focus on when writing version 2 of phpScheduleIt was finding ways to keep the application open for extensibility. This led me to creating a pluggable framework that can be hooked into for certain operations. In this post I’ll explain a bit of the plugin structure and walk through how to configure and use plugins in phpScheduleIt.

Note: The version of phpScheduleIt at the time of writing is 2.2. While the exact details explained in this post may not apply to future versions, the core concepts will.

Plugin Overview

Let’s first explore how plugins are structured. phpScheduleIt ships with a /plugins directory. Within that are subdirectories for each supported plugin type. phpScheduleIt’s plugin architecture is mainly convention based. Each subdirectory within a plugin type represents a plugin. Taking the Authentication plugin type for example, we have ActiveDirectory, Drupal, and so on available to use for our authentication provider. The Authentication plugins directory currently looks like this:

Configuring and Activating a Packaged Plugin

phpScheduleIt comes packaged with a a few Authentication plugins, so we’ll continue using that for our example. Each plugin directory most likely will contain a default configuration file named something like subdirectory.config.dist.php. For example, ActiveDirectory will contain a file named ActiveDirectory.config.dist.php.

The first step is to remove the .dist part of the default configuration file. In our case, the file would end up being named ActiveDirectory.config.php.

Next, you need to adjust any configuration values to match your environment. For Active Directory, you may need to change domain controllers or administrative credentials.

Finally, you need to tell phpScheduleIt which plugin you want to use. Open up phpScheduleIt’s config file, located in /config/config.php and find the plugins section. By default, it looks like this:

$conf['settings']['plugins']['Authentication'] = '';
$conf['settings']['plugins']['Authorization'] = '';
$conf['settings']['plugins']['Permission'] = '';
$conf['settings']['plugins']['PostRegistration'] = '';
$conf['settings']['plugins']['PreReservation'] = '';
$conf['settings']['plugins']['PostReservation'] = '';

To use your configured plugin, just set the plugin value to the plugin name. Sticking with ActiveDirectory for Authorization, you’ll end up with this:

$conf['settings']['plugins']['Authentication'] = 'ActiveDirectory ';

And that’s it! phpScheduleIt will now use Active Directory when users log in.

Using a 3rd Party Plugin

The process for using a 3rd party plugin is nearly the same as the using a packaged plugin. The one difference is that the plugin needs to be ‘installed’ – which merely includes copying the full plugin directory into the plugin type directory. For example, if you are installing an Authentication plugin named FooAuth, simply copy the full FooAuth directory into /plugins/Authentication

Summary

Hopefully this process is nice and easy. Next we’ll talk about how to write our own plugins, starting with a pre-reservation plugin.