Graham Christensen

Symfony development, hardware, and everything else too.

Validate a Domain is Valid and Exists, Symfony 1.2

After spending good portion of my day validating, parsing, and analyzing URLs - I find that I’ve written a small set of tools to ensure consistency, clean, and sane URLs. After an overhaul on a settings page - I had to integrate much of this into a simple (to the viewer, anyway) form. To ensure that user-entered URLs are valid, I’ve written a custom validator in Symfony 1.2 to accomplish this validation.

Writing a Custom sfValidator

Writing a custom validator really isn’t as difficult as you would imagine. Open up one of the existing ones, and you’ll see that. Primarily, you only need to worry about two methods.

configure()

The configure method is where you can specify customization options, making some required or optional. This method is used to build flexibility into the validator.

doClean()

The doClean method may seem confusing at first, however it’s very simple. Upon failure, throw an exception.

How to Signal a Failure
<?php
throw new sfValidatorError($this, 'invalid', array('value' => $value));
?>

invalid is the name of the message you want to error with, and the options array at in the third parameter allow you to replace variables in the message.

How to Handle a Success

If the validator is successful - you have two options:

  • Your validator can act as a filter, and return something different from the input value
  • Your validator can return exactly what it was passed in.

Implementing the new sfValidator into an sfForm

This validator is used just like any other:

<?php
$form->setValidator('domain', new sfValidatorDomain());
?>
<?php
/**
 * @author Graham Christensen <graham@grahamc.com>
*/
class sfValidatorDomain extends sfValidatorBase
{
  /**
   * Configures the current validator.
   *
   *
   * @param array $options    An array of options
   * @param array $messages   An array of error messages
   *
   * @see sfValidatorBase
   */
  protected function configure($options = array(),
                                $messages = array()) {
      // The type of method to use to validate it.
      $this->addOption('clean_type', 'url');

      // List of valid protocol schemes to allow in URLs
      $this->addOption('schemes', array('http', 'https'));

      // The default scheme
      $this->addOption('default_scheme', 'http');

	  // Setup some basic error messages
	  $msg = 'The provided domain does not appear to be valid.';
      $this->addMessage('badform', $msg);
      $this->addMessage('badscheme', $msg);
      $this->addMessage('nohost', $msg);
      $this->addMessage('invalid', $msg);
  }

  /**
   * @see sfValidatorBase
   */
  protected function doClean($value) {
      if ($this->getOption('clean_type') == 'domain') {
          // If it's a domain, then it's simple to check it.
          $domain = $value;
      } else {
          // It's probably a complete URL, so check it in
          // more depth.

          // Verify that it can be parsed as a URL.
          // Note: @'s are bad practice, however if a method is
          // being checked and we can't stop the error, then
          // we want to hide it.

          $parts = @parse_url($value); // May throw a warning

          // If there is no scheme (http, https, etc.) then it's
          // likely that parse_url parsed it incorrectly, so
          // prepend a scheme and try again. if we don't do this,
          // we may get "example.com/foobar" as our path.
          if (!isset($parts['scheme'])) {
              $value = $this->getOption('default_scheme')
                       . '://' . $value;
              $parts = @parse_url($value);
          }

          // If it wasn't parsed, then something was wrong.
          if (!$parts) {
              throw new sfValidatorError($this, 'badform',
                            array('value' => $value));
          }

          // Validate that the scheme provided is valid
          if (!in_array($parts['scheme'],
                        $this->getOption('schemes'))) {
              throw new sfValidatorError($this, 'badscheme',
                            array('value' => $value));
          }

          // Ensure that the host was found
          if (!isset($parts['host'])) {
              throw new sfValidatorError($this, 'nohost',
                            array('value' => $value));
          } else {
              // Finally set the domain for the final, unified
              // verification.
              $domain = $parts['host'];
          }
      }

      // Convert the domain to an IP address
      $ip_address = gethostbyname($domain);

      // Unfortunately, gethostbyname's only response if it
      // fails, is returns the input $domain. Try to convert it
      // to a packed IP address. If that fails, then it isn't a
      // valid domain name.
      if (@inet_pton($ip_address)) {
          return $value;
      }

      // Didn't validate...
    throw new sfValidatorError($this, 'invalid',
                    array('value' => $value));
  }
}
?>

Install this into lib/validator/sfValidatorDomain.class.php

comments · posted on October 27 2009

Modifying Form Elements in Symfony 1.2

As I found in recent development of an app, I needed to change one of Symfony’s form on the fly (more specifically, I needed to change a drop-down, sfWidgetFormSelect). After some looking, there wasn’t much documentation on this that I could find. My final solution was easy enough:

<?php
$form = new SomeFancyForm();
$new_choices = array('Selection 1', 'Selection 2', 'Selection 3');
$widget = $form->getWidget('select_widget_name');
$widget->setOption('choices', $new_choices);
?>

Of course, that can easily be adapted to modify any option on any form - just find the appropriate option you want to adjust. Additionally, you may need to adjust the validator as well.

Relevant Documentation:

comments · posted on October 15 2009

Forcing Login Success Page, Symfony 1.2 + sfGuard

Working on my latest project, we needed to force a specific page to be sent to after login. After quite a bit of searching, I went to the most logical location for this information: The README. Duh. sfGuardPlugin v3.1.3 Readme

Unfortunately, Symfony’s documentation is notoriously sketchy, however this is verifiably functional.

Add the following to your app.yml:

all:
  sf_guard_plugin:
    success_signin_url:      '@my_route?param=value'
    success_signout_url:     module/action
comments · posted on September 28 2009

Ranking Items in a Database

At work recently, I was tasked to create a system that ranked items in a database, from least to greatest based on a time measurement. Originally I was quite blinded by the original code, which had used three nested queries, and a dozen variables of impenetrable names, a set of code I won’t be posting. Now, this isn’t a very hard task - but let me document the path through with I got to the simple solution. This idea wasn’t immediately obvious to me, or someone else who had quite a bit more experience on the topic - but here we are.

The Dataset

The dataset that I’m ranking is much more complex, however the concept is still the same.

CREATE TABLE `response` (
	id INT(11) AUTO_INCREMENT NOT NULL,
	time FLOAT NOT NULL,
	meta VARCHAR(255),
	PRIMARY KEY(id)
);

Temporary Tables…Ish.

My first idea was to have a table that would be regenerated every $increment (probably an hour, maybe a minute when we were small.) These tables would be pretty simple, just:

CREATE TABLE `response_ranking` (
	id INT(11) AUTO_INCREMENT NOT NULL,
	response_id INT(11) NOT NULL,
	PRIMARY KEY(id),
	FOREIGN KEY (response_id) REFERENCES `response` (id)
);

And then in order to populate the database with the appropriate ranks, we would run:

-- Truncate the table
TRUNCATE TABLE response_rank;

-- Put the data into the table in order by time.
INSERT INTO response_rank (response_id, time)
SELECT id AS response_id, time
FROM response
ORDER BY time;

The SQL code would take the data from the response table in order of time, fastest to slowest, and then insert it into the response_rank table. The response_rank table’s ID would then directly be their rank amongst that dataset. The first record would be the fastest, the 257th record is the 257th fastest, etc. In order to find the rank for a particular record, the query is quite simple too:

SELECT id AS rank
FROM response_rank
WHERE response_id = ?;

Why This is a Bad Idea (tm)

For the number of records I’ll have and the size of the site, caching the data isn’t necessary. Adding that level of complexity to a smaller site isn’t worth the potential scalability it may provide.

How I Did It

Now this may not be the most efficient way to do it, however I imagine it’s much closer to a Good Idea (tm).

Instead of regenerating indexes or pushing massive amounts of data, let’s use a simple database operation which the database can be easily optimized to do, if it isn’t already by adding an index to the time column.

I present to you, The Better Way:

SELECT count(1) AS rank
FROM response
WHERE time < ?;

When you run the query, rank is exactly the rank within the current dataset, with nothing waiting to be aggregated into the database, or an index needing to be regenerated.

Now I know, this is stupidly simple, but it wasn’t extremely obvious to me or my coworker - so maybe someone else is stuck on such a simple problem too. However, this also may not be the most efficient way to do it either - if there’s a better way, I’d love to know.

comments · posted on September 05 2009

Forcing SSL and HTTPS with Redirects on Symfony 1.2, 1.3, 1.4

Ensure security of specific URLs using Symfony 1.2, 1.3, and 1.4 to protect from data being leaked. Read more...

posted on September 01 2009