Datamorph is a Laravel package that allows you to create and run Flow PHP ETL (Extract, Transform, Load) pipelines in a structured and extensible way. This documentation will guide you through the installation, configuration, and usage of the package.
- Installation
- Concepts
- Configuration
- Creating an ETL Pipeline
- Running an ETL Pipeline
- Hooks
- Concrete Examples
Install the package via Composer:
composer require pollora/datamorphDatamorph is built around three main components:
- Extractors: Retrieve data from various sources (databases, APIs, files, etc.)
- Transformers: Transform the retrieved data according to your needs
- Loaders: Load the transformed data to their final destination
These three components are orchestrated in a Pipeline that also manages Hooks that allow you to intervene at different stages of the process.
Publish the configuration file:
php artisan vendor:publish --tag=datamorph-configThis will create a config/datamorph.php file where you can configure your ETL pipelines:
return [
'pipelines' => [
'stock' => [
'hooks' => [
'before_extract' => [
App\ETL\Stock\Hooks\BeforeStockExtract::class,
],
'after_extract' => [
// Hooks to execute after extraction
],
'before_transform' => [
// Hooks to execute before transformation
],
'after_transform' => [
// Hooks to execute after transformation
],
'before_load' => [
// Hooks to execute before loading
],
'after_load' => [
// Hooks to execute after loading
],
'before_run' => [
App\ETL\Stock\Hooks\BeforeStockRun::class,
],
'after_run' => [
// Hooks to execute after complete execution
],
],
],
// Other pipelines...
],
];Datamorph includes an Artisan command that automatically generates the necessary files for a new pipeline:
php artisan datamorph:make productThis command will create the following files in the app/ETL/Product/ directory:
ProductExtractor.php- For data extractionProductTransformer.php- For data transformationProductLoader.php- For loading transformed data
<?phpdeclare(strict_types=1);
namespaceApp\ETL\Product;
useFlow\ETL\FlowContext;
usePollora\Datamorph\Contracts\Extractor;
class ProductExtractor extends Extractor
{
publicfunctionhandle(FlowContext$context): array
{
// Data extraction logic// Returns an array of raw datareturn [];
}
}<?phpdeclare(strict_types=1);
namespaceApp\ETL\Product;
useFlow\ETL\FlowContext;
usePollora\Datamorph\Contracts\Transformer;
class ProductTransformer extends Transformer
{
publicfunctionhandle(array$rows, FlowContext$context): array
{
// Data transformation logic// Receives raw data and returns transformed datareturn$rows;
}
}<?phpdeclare(strict_types=1);
namespaceApp\ETL\Product;
useFlow\ETL\Rows;
useFlow\ETL\FlowContext;
usePollora\Datamorph\Contracts\Loader;
useFlow\ETL\LoaderasFlowLoader;
class ProductLoader extends Loader
{
publicfunctionhandle(FlowContext$context): FlowLoader
{
// Data loading logic// Returns a Flow ETL loaderreturnto_memory();
}
}Once your components are implemented and your pipeline is configured, you can run it with the Artisan command:
php artisan datamorph:run stockThis command:
- Checks that the pipeline exists in the configuration
- Checks that the Extractor, Transformer, and Loader classes exist
- Instantiates these classes and creates a Pipeline
- Runs the Pipeline with the configured hooks
You can also run a pipeline programmatically:
usePollora\Datamorph\Pipeline;
useApp\ETL\Stock\StockExtractor;
useApp\ETL\Stock\StockTransformer;
useApp\ETL\Stock\StockLoader;
$pipeline = newPipeline(
'stock',
newStockExtractor(),
newStockTransformer(),
newStockLoader()
);
$pipeline->run();Hooks are a powerful mechanism in Datamorph that allows you to intervene at different stages of an ETL pipeline. There are three ways to implement hooks in Datamorph, each with its own use cases.
The first approach is to define hooks in the config/datamorph.php configuration file. This method is ideal for recurring hooks that need to be applied to every pipeline execution.
// config/datamorph.phpreturn [
'pipelines' => [
'stock' => [
'hooks' => [
'before_extract' => [
App\ETL\Stock\Hooks\BeforeStockExtract::class,
],
'after_extract' => [
App\ETL\Stock\Hooks\AfterStockExtract::class,
],
'before_transform' => [
App\ETL\Stock\Hooks\BeforeStockTransform::class,
],
'after_transform' => [
App\ETL\Stock\Hooks\AfterStockTransform::class,
],
'before_load' => [
App\ETL\Stock\Hooks\BeforeStockLoad::class,
],
'after_load' => [
App\ETL\Stock\Hooks\AfterStockLoad::class,
],
'before_run' => [
App\ETL\Stock\Hooks\BeforeStockRun::class,
],
'after_run' => [
App\ETL\Stock\Hooks\AfterStockRun::class,
],
],
],
],
];Each hook must implement the HookInterface:
<?phpnamespaceApp\ETL\Stock\Hooks;
useClosure;
useFlow\ETL\DataFrame;
useFlow\ETL\Filesystem\SaveMode;
usePollora\Datamorph\Contracts\HookInterface;
class BeforeStockRun implements HookInterface
{
/** * Execute the hook with the given dataframe. * * @param mixed $dataframe The dataframe to process * @param Closure|null $next The next hook to execute * @return mixed */publicfunctionhandle(mixed$dataframe, ?Closure$next = null): mixed
{
// Apply hook logicif ($dataframeinstanceof DataFrame) {
$dataframe = $dataframe->mode(SaveMode::Overwrite);
}
// Pass to the next hook in the chainreturn$next ? $next($dataframe) : $dataframe;
}
}The second approach uses the $context->pipeline->after() methods to register hooks dynamically during pipeline execution. This method is particularly useful for conditional behaviors or hooks that depend on the current state of the pipeline.
// In an ETL component (Extractor, Transformer, Loader)publicfunctionhandle(FlowContext$context): array
{ // Add a hook after the current operation$context->pipeline->after(function ($dataframe) {
// Hook logic
log::info("After extraction");
return$dataframe;
}); // The operation is automatically detected// ...
}You can pass different types of hooks to the after() method:
- A Closure (anonymous function) - Will be automatically wrapped in a
DynamicHook - An instance of a class implementing
HookInterface- Will be used directly - A class name - The class will be resolved via Laravel's IoC container
If you don't explicitly specify the operation, Datamorph will detect it automatically based on the calling context:
- In an
Extractor, the operation will beextract - In a
Transformer, the operation will betransform - In a
Loader, the operation will beload
The third approach is to directly implement the before() and after() methods in your Extractor, Transformer, and Loader classes. This method is the simplest and most direct for standard behaviors.
<?phpnamespaceApp\ETL\Stock;
useFlow\ETL\FlowContext;
useIlluminate\Support\Facades\Log;
usePollora\Datamorph\Contracts\Extractor;
class StockExtractor extends Extractor
{
/** * Extract stock data. */publicfunctionhandle(FlowContext$context): array
{
// Extraction logicreturn$results;
}
/** * Method executed before extraction. */publicfunctionbefore(mixed$dataframe, FlowContext$context): mixed
{
Log::info("Preparing extraction");
return$dataframe;
}
/** * Method executed after extraction. */publicfunctionafter(mixed$dataframe, FlowContext$context): mixed
{
Log::info("Extraction completed");
return$dataframe;
}
}The before() and after() methods are automatically called by the pipeline at the appropriate times, without any additional configuration.
All three approaches can be combined in the same pipeline. The execution order is as follows:
- Hooks configured in
config/datamorph.php - The
before()method of the relevant ETL component - Main operation (extraction, transformation, loading)
- The
after()method of the relevant ETL component - Dynamic hooks registered via
$pipeline->before()and$pipeline->after()
This combination offers great flexibility and can address a variety of use cases.
// config/datamorph.php'hooks' => [
'before_extract' => [
App\ETL\Stock\Hooks\ValidateSourceHook::class,
],
]// App\ETL\Stock\Hooks\ValidateSourceHook.phppublicfunctionhandle(mixed$dataframe, ?Closure$next = null): mixed
{
// Check if the data source is availableif (!$this->isSourceAvailable()) {
thrownew \RuntimeException("Data source is not available");
}
return$next($dataframe);
}// In StockExtractorpublicfunctionhandle(FlowContext$context): array
{
// Add logging hooks if in debug modeif (config('app.debug')) {
$context->pipeline->before(function ($dataframe) {
Log::debug("Starting transformation");
return$dataframe;
}, 'transform');
$context->pipeline->after(function ($dataframe) {
Log::debug("Transformation completed");
return$dataframe;
}, 'transform');
}
// ...
}// In DatabaseExtractorpublicfunctionbefore(mixed$dataframe, FlowContext$context): mixed
{
// Open database connection$this->connection = DB::connection('source');
Log::info("Database connection established");
return$dataframe;
}
publicfunctionafter(mixed$dataframe, FlowContext$context): mixed
{
// Close connection after extractionif ($this->connection) {
$this->connection = null;
Log::info("Database connection closed");
}
return$dataframe;
}// config/datamorph.php - Global hooks'hooks' => [
'before_run' => [
App\ETL\Global\Hooks\LogStartHook::class,
],
'after_run' => [
App\ETL\Global\Hooks\LogEndHook::class,
],
]
// StockExtractor.php - Before/After methods
public functionbefore(mixed $dataframe, FlowContext $context): mixed
{
// Extraction-specific preparationreturn$dataframe;
}
// In a component's handle method - Dynamic hooks
public functionhandle(FlowContext $context): array
{
// Dynamic hook for a specific caseif ($someCondition) {
$context->pipeline->after(function ($dataframe) {
// Conditional logicreturn$dataframe;
});
}
// ...
}This combination of approaches provides you with a flexible and powerful hook system capable of addressing a variety of needs in your ETL pipelines.