The Project

I'm currently building a service that needs to handle a variety of file formats.

These include:

  • CSV – Comma-Separated Values

  • RSS – Really Simple Syndication

  • TXT – Plain Text Files

  • XML – Extensible Markup Language

  • JSON – JavaScript Object Notation

Each format has its own structure and quirks, the service will need to process them accordingly and return a collection of data.

So how would I approach this?

The Factory

I’m thinking, I need a Factory, which will accept a file path and return an appropriate processing class, which then returns a collection of data.

Not sure what a factory is? Check out my Factory pattern article.

I like to create placeholder tests first. I’ve created a FactoryTest.php file (I’m using Pest by the way) which initially contains:

test('returns csv processor class correctly', function () {
    //
})->todo();

test('returns txt processor class correctly', function () {
    //
})->todo();

test('returns xml processor class correctly', function () {
    //
})->todo();

test('returns rss processor class correctly', function () {
    //
})->todo();

test('returns json processor class correctly', function () {
    //
})->todo();

When running the test suite, we get:

Perfect - thats what we want! The first order of business is to create some test fixtures of each different type of file.

With these in place. Let’s start writing how I’d want this code to look.

test('returns csv processor class correctly', function () {
    $class = FeedProcessorFactory::make("./tests/fixtures/sample.csv");
    expect($class)->toBeInstanceOf(FeedProcessor::class);
    expect($class)->toBeInstanceOf(CSVProcessor::class);
});

test('returns txt processor class correctly', function () {
    $class = FeedProcessorFactory::make("./tests/fixtures/sample.txt");
    expect($class)->toBeInstanceOf(FeedProcessor::class);
    expect($class)->toBeInstanceOf(TXTProcessor::class);
});

test('returns xml processor class correctly', function () {
    $class = FeedProcessorFactory::make("./tests/fixtures/sample.xml");
    expect($class)->toBeInstanceOf(FeedProcessor::class);
    expect($class)->toBeInstanceOf(XMLProcessor::class);
});

test('returns rss processor class correctly', function () {
    $class = FeedProcessorFactory::make("./tests/fixtures/sample.rss");
    expect($class)->toBeInstanceOf(FeedProcessor::class);
    expect($class)->toBeInstanceOf(RssProcessor::class);
});

test('returns json processor class correctly', function () {
    $class = FeedProcessorFactory::make("./tests/fixtures/sample.json");
    expect($class)->toBeInstanceOf(FeedProcessor::class);
    expect($class)->toBeInstanceOf(JsonProcessor::class);
});

This is going to break, none of this exists BUT I can now write the code which will eventually cause these tests to pass.

See? Nothing works. It’s telling us FeedProcessorFactory doesn’t exist. Let’s fix that:

<?php

namespace App\Services\FeedProcessor;

class FeedProcessorFactory
{

}

// Add import to test
use App\Services\FeedProcessor\FeedProcessorFactory;

Run the test again:

We’re working through things.

Let’s add the function it’s telling us doesn’t exist - make(string $filePath).

namespace App\Services\FeedProcessor;

class FeedProcessorFactory
{
    public static function make(string $filePath)
    {

    }
}

Run the tests again etc etc etc. You get the picture, right? I keep doing this. Next error I get is:

UnknownClassOrInterfaceException
Class or interface "FeedProcessor" does not exist

Great, lets create the Interface and import it.

namespace App\Services\FeedProcessor\Interface;

interface FeedProcessor
{
 
}

// Test File
use App\Services\FeedProcessor\Interface\FeedProcessor;

Next error:

Failed asserting that null is an instance of interface App\Services\FeedProcessor\Interface\FeedProcessor.

This is telling us the make method isn’t returning an instance of FeedProcessor, so lets fix the return type.

public static function make(string $filePath): FeedProcessor
{

}

You got it… let’s run the tests again.

App\Services\FeedProcessor\FeedProcessorFactory::make(): Return value must be of type App\Services\FeedProcessor\Interface\FeedProcessor, none returned

We need some concrete classes now, lets create classes for each type.

class CsvProcessor implements FeedProcessor
{
    //
}

class JsonProcessor implements FeedProcessor
{
    //
}

class RssProcessor implements FeedProcessor
{
    //
}

class TxtProcessor implements FeedProcessor
{
    //
}

class XmlProcessor implements FeedProcessor
{
    //
}

Perfect! Let’s change the make method return one of them (so we get past the current error).

Maybe something like this?

public static function make(string $filePath): FeedProcessor
{
    return new CsvProcessor();
}

Now let’s import these into our test:

use App\Services\FeedProcessor\FeedProcessorFactory;
use App\Services\FeedProcessor\Interface\FeedProcessor;
use App\Services\FeedProcessor\Processors\CsvProcessor;
use App\Services\FeedProcessor\Processors\TxtProcessor;
use App\Services\FeedProcessor\Processors\XmlProcessor;
use App\Services\FeedProcessor\Processors\RssProcessor;
use App\Services\FeedProcessor\Processors\JsonProcessor;

test('returns csv processor class correctly', function () {
    $class = FeedProcessorFactory::make("./tests/fixtures/sample.csv");
    expect($class)->toBeInstanceOf(FeedProcessor::class);
    expect($class)->toBeInstanceOf(CsvProcessor::class);
});

test('returns txt processor class correctly', function () {
    $class = FeedProcessorFactory::make("./tests/fixtures/sample.txt");
    expect($class)->toBeInstanceOf(FeedProcessor::class);
    expect($class)->toBeInstanceOf(TxtProcessor::class);
});

test('returns xml processor class correctly', function () {
    $class = FeedProcessorFactory::make("./tests/fixtures/sample.xml");
    expect($class)->toBeInstanceOf(FeedProcessor::class);
    expect($class)->toBeInstanceOf(XmlProcessor::class);
});

test('returns rss processor class correctly', function () {
    $class = FeedProcessorFactory::make("./tests/fixtures/sample.rss");
    expect($class)->toBeInstanceOf(FeedProcessor::class);
    expect($class)->toBeInstanceOf(RssProcessor::class);
});

test('returns json processor class correctly', function () {
    $class = FeedProcessorFactory::make("./tests/fixtures/sample.json");
    expect($class)->toBeInstanceOf(FeedProcessor::class);
    expect($class)->toBeInstanceOf(JsonProcessor::class);
});

And now run the tests (again):

WE HAVE A PASS! But the other tests are failing, this is because we’re returning the CsvProcessor class every time!

Let’s change that.

namespace App\Services\FeedProcessor;

use App\Services\FeedProcessor\Interface\FeedProcessor;
use App\Services\FeedProcessor\Processors\CsvProcessor;
use App\Services\FeedProcessor\Processors\JsonProcessor;
use App\Services\FeedProcessor\Processors\RssProcessor;
use App\Services\FeedProcessor\Processors\TxtProcessor;
use App\Services\FeedProcessor\Processors\XmlProcessor;
use InvalidArgumentException;

class FeedProcessorFactory
{
    public static function make(string $filePath): FeedProcessor
    {
        $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));

        return match ($extension) {
            'csv'  => new CsvProcessor($filePath),
            'rss'  => new RssProcessor($filePath),
            'txt'  => new TxtProcessor($filePath),
            'xml'  => new XmlProcessor($filePath),
            'json' => new JsonProcessor($filePath),
            default => throw new InvalidArgumentException("Unsupported file type: {$extension}"),
        };
    }
}

And run the tests…

And we’re done….

… or are we?

What if I pass in a file which doesn’t exist?
What if I pass in a file with an invalid extension?

We don’t have any tests for those. It’s important to test for failures too. We want our code to fail correctly.

Let’s add the tests.

test('throws exception when file does not exist', function () {
    FeedProcessorFactory::make("nonexistent/path/file.csv");
})->throws(InvalidArgumentException::class, 'File not found: nonexistent/path/file.csv');

test('throws exception when file has an invalid extension', function () {
    FeedProcessorFactory::make("./tests/fixtures/sample.png");
})->throws(InvalidArgumentException::class, 'Unsupported file type: png');

And…. You guessed it! We have a failure.

Let’s change the code so the tests pass.

namespace App\Services\FeedProcessor;

use App\Services\FeedProcessor\Interface\FeedProcessor;
use App\Services\FeedProcessor\Processors\CsvProcessor;
use App\Services\FeedProcessor\Processors\JsonProcessor;
use App\Services\FeedProcessor\Processors\RssProcessor;
use App\Services\FeedProcessor\Processors\TxtProcessor;
use App\Services\FeedProcessor\Processors\XmlProcessor;
use InvalidArgumentException;

class FeedProcessorFactory
{
    public static function make(string $filePath): FeedProcessor
    {
        if (!file_exists($filePath)) {
            throw new InvalidArgumentException("File not found: {$filePath}");
        }

        $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));

        return match ($extension) {
            'csv'  => new CsvProcessor($filePath),
            'rss'  => new RssProcessor($filePath),
            'txt'  => new TxtProcessor($filePath),
            'xml'  => new XmlProcessor($filePath),
            'json' => new JsonProcessor($filePath),
            default => throw new InvalidArgumentException("Unsupported file type: {$extension}"),
        };
    }
}

And here we go.

What’s next?

I’d say we need to create tests for each concrete class (and the Interface).

App\Services\FeedProcessor\Interface\FeedProcessor;
App\Services\FeedProcessor\Processors\CsvProcessor;
App\Services\FeedProcessor\Processors\JsonProcessor;
App\Services\FeedProcessor\Processors\RssProcessor;
App\Services\FeedProcessor\Processors\TxtProcessor;
App\Services\FeedProcessor\Processors\XmlProcessor;

And ensure they’re doing what they should be.

But this article is finished!

Watch out for Part 2. But yeah, this is how I code, my dude.