Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ composer require keboola/php-component
Create a subclass of `BaseComponent`.

```php
<?php
class Component extends \Keboola\Component\BaseComponent
{
public function run(): void
protected function run(): void
{
// get parameters
$parameters = $this->getConfig()->getParameters();
Expand All @@ -41,7 +42,7 @@ class Component extends \Keboola\Component\BaseComponent
(new OutFileManifestOptions())
->setTags(['tag1', 'tag2'])
);

// write manifest for output table
$this->getManifestManager()->writeTableManifest(
'data.csv',
Expand All @@ -50,8 +51,17 @@ class Component extends \Keboola\Component\BaseComponent
->setDestination('out.report')
);
}
}

protected function customSyncAction(): array
{
return ['result' => 'success', 'data' => ['joe', 'marry']];
}

protected function getSyncActions(): array
{
return ['custom' => 'customSyncAction'];
}
}
```

Use this `src/run.php` template.
Expand All @@ -68,7 +78,7 @@ require __DIR__ . '/../vendor/autoload.php';
$logger = new Logger();
try {
$app = new MyComponent\Component($logger);
$app->run();
$app->execute();
exit(0);
} catch (\Keboola\Component\UserException $e) {
$logger->error($e->getMessage());
Expand All @@ -88,6 +98,15 @@ try {
}
```

## Sync actions support

[Sync actions](https://developers.keboola.com/extend/common-interface/actions/) can be called directly via API. API will block and wait for the result. The correct action is selected based on the `action` key of config. `BaseComponent` class handles the selection automatically. Also it handles serialization and output of the action result - sync actions must output valid JSON.

To implement a sync action
* add a method in your `Component` class. The naming is entirely up to you.
* override the `Component::getSyncActions()` method to return array containing your sync actions names as keys and corresponding method names from the `Component` class as values.
* return value of the method will be serialized to json

## Customizing config

### Custom getters in config
Expand Down Expand Up @@ -158,6 +177,10 @@ class MyComponent extends \Keboola\Component\BaseComponent

If any constraint of config definition is not met a `UserException` is thrown. That means you don't need to handle the messages yourself.

## Migration from version 6 to version 7

The default entrypoint of component (in `index.php`) changed from `BaseComponent::run()` to `BaseComponent::execute()`. While running the component via `run` method is still supported, you need to use `execute()` if you want to take advantage of sync action support.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to je zoufale nahovno bcbreak. run.php se testuje zpravidla jen integracnima testama a ty zase zpravidla netestujou sync akce. to znamena, ze kdyz updatnes komponentu a zapomenes ji zmenit run.php tak testy projdou, ale sync akce nebudou fungovat.

premyslim jak z toho vybruslit a nejjednodussi se mi zda, prejmenovat run na runAction, coz se me sice nelibi, protoze se pak trosku zbytecne bude plest se sync akcema, ale udela to poradnej bc break, kterej zajisti, ze se pokazenej run.php nedostane do produkce.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

taky bych do bc breaku pridal, ze testovaci konfigurace musi mit run, v testech to casto chybi, protoze to melo default hodnotu

Copy link
Contributor Author

@tomasfejfar tomasfejfar Mar 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

taky bych do bc breaku pridal, ze testovaci konfigurace musi mit run, v testech to casto chybi, protoze to melo default hodnotu

Myšlenka byla, že když na tom teď závisíme (předtím jsme nikde v kódu run metodu nevyžadovali) tak bysme měli run vyžadovat.

Ale na druhou stranu si říkám, že by asi bylo lepší ten BC break udělat co nejmenší - aby to byl co nejmenší pain upgradovat.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prejmenovat run na runAction

Což ale neudělá nic. MyComponent::run() furt existuje a furt se volá z indexu. A nemusí volat parent::run(), takže ani kdybych tam dal exceptionu, tak to nepomůže.

Napadá mě v konstruktoru přes reflexi zkontrolovat, že run není public ale protected. Když bude protected, tak nejde zavolat z indexu a problem solved. A komponenty určitě parent constructor volají. Jinak by přišli o většinu cool věcí, co component umí :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zkusím to vykopnout a pushnout.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no tak z teho komentara su jelen :)

A nemusí volat parent::run(),
A komponenty určitě parent constructor volají.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

z komentare jsem jelen, ale myslim, ze by to tak mohlo byt

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ve zkratce:

  1. v index.php mám $myComponent->run()
  2. přejmenuju Component::run() na Component::runAction()
  3. MyComponent::run se nepřejmenuje, takže volání v 1. stále proběhne jako by nic

Oproti tomu, když něco přidám do konstruktoru, tak tam je slušná jistota, že se to zavolá i v poděděné komponentě.

To jsem tím chtěl říct.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MyComponent::run se nepřejmenuje, takže volání v 1. stále proběhne jako by nic

neprobehene, protoze neexistuje parent::run()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Což předpokládá, že v metodě mycomponent::run se zavolá parent::run. Což myslím, že se moc neděje.

## More reading

For more information, please refer to the [generated docs](https://keboola.github.io/php-component/master/classes.html). See [development guide](https://developers.keboola.com/extend/component/tutorial/) for help with KBC integration.
12 changes: 11 additions & 1 deletion example/Component.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

class Component extends \Keboola\Component\BaseComponent
{
public function run(): void
protected function run(): void
{
// get parameters
$parameters = $this->getConfig()->getParameters();
Expand Down Expand Up @@ -41,4 +41,14 @@ public function run(): void
->setDestination('out.report')
);
}

protected function customSyncAction(): array
{
return ['result' => 'success', 'data' => ['joe', 'marry']];
}

protected function getSyncActions(): array
{
return ['custom' => 'customSyncAction'];
}
}
2 changes: 1 addition & 1 deletion example/run.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
$logger = new Logger();
try {
$app = new MyComponent\Component($logger);
$app->run();
$app->execute();
exit(0);
} catch (\Keboola\Component\UserException $e) {
$logger->error($e->getMessage());
Expand Down
76 changes: 74 additions & 2 deletions src/BaseComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@
namespace Keboola\Component;

use ErrorException;
use Exception;
use Keboola\Component\Config\BaseConfig;
use Keboola\Component\Config\BaseConfigDefinition;
use Keboola\Component\Exception\BaseComponentException;
use Keboola\Component\Logger\AsyncActionLogging;
use Keboola\Component\Logger\SyncActionLogging;
use Keboola\Component\Manifest\ManifestManager;
use Monolog\Handler\NullHandler;
use Psr\Log\LoggerInterface;
use Reflection;
use ReflectionClass;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
use function error_reporting;
Expand Down Expand Up @@ -43,10 +50,13 @@ public function __construct(LoggerInterface $logger)
$this->setDataDir($dataDir);

$this->loadConfig();
$this->initializeSyncActions();
$this->loadInputState();

$this->loadManifestManager();

$this->checkRunMethodNotPublic();

$this->logger->debug('Component initialization completed');
}

Expand Down Expand Up @@ -85,7 +95,27 @@ protected function loadConfig(): void
} catch (InvalidConfigurationException $e) {
throw new UserException($e->getMessage(), 0, $e);
}
$this->logger->debug('Config loaded');
}

protected function initializeSyncActions(): void
{
if (array_key_exists('run', $this->getSyncActions())) {
throw BaseComponentException::runCannotBeSyncAction();
}
foreach ($this->getSyncActions() as $method) {
if (!method_exists($this, $method)) {
throw BaseComponentException::invalidSyncAction($method);
}
}
if ($this->isSyncAction()) {
if ($this->logger instanceof SyncActionLogging) {
$this->logger->setupSyncActionLogging();
}
} else {
if ($this->logger instanceof AsyncActionLogging) {
$this->logger->setupAsyncActionLogging();
}
}
}

protected function loadInputState(): void
Expand All @@ -97,6 +127,15 @@ protected function loadInputState(): void
}
}

private function checkRunMethodNotPublic(): void
{
$reflection = new ReflectionClass(static::class);
$method = $reflection->getMethod('run');
if ($method->isPublic()) {
throw BaseComponentException::runMethodCannotBePublic();
}
}

protected function writeOutputStateToFile(array $state): void
{
JsonHelper::writeFile(
Expand Down Expand Up @@ -160,11 +199,28 @@ public function getInputState(): array
return $this->inputState;
}

public function execute(): void
{
if (!$this->isSyncAction()) {
$this->run();
return;
}

$action = $this->getConfig()->getAction();
$syncActions = $this->getSyncActions();
if (array_key_exists($action, $syncActions)) {
$method = $syncActions[$action];
echo JsonHelper::encode($this->$method());
} else {
throw BaseComponentException::invalidSyncAction($action);
}
}

/**
* This is the main method for your code to run in. You have the `Config`
* and `ManifestManager` ready as well as environment set up.
*/
public function run(): void
protected function run(): void
{
// to be implemented in subclass
}
Expand All @@ -186,4 +242,20 @@ protected function loadManifestManager(): void
{
$this->manifestManager = new ManifestManager($this->dataDir);
}

public function isSyncAction(): bool
{
return $this->getConfig()->getAction() !== 'run';
}

/**
* Whitelist method names that can be used as synchronous actions. This is a
* safeguard against executing any method of the component.
*
* Format: 'action' => 'method name' (e.g. 'getTables' => 'handleTableSyncAction')
*/
protected function getSyncActions(): array
{
return [];
}
}
28 changes: 28 additions & 0 deletions src/Exception/BaseComponentException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Keboola\Component\Exception;

use Exception;

class BaseComponentException extends Exception
{
public static function invalidSyncAction(string $action): self
{
return new self(sprintf(
'Unknown sync action "%s", method does not exist in class',
$action
));
}

public static function runCannotBeSyncAction(): self
{
return new self('"run" cannot be a sync action');
}

public static function runMethodCannotBePublic(): self
{
return new self('Method "run" cannot be public since version 7');
}
}
68 changes: 24 additions & 44 deletions src/Logger.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,54 +5,11 @@
namespace Keboola\Component;

use Monolog\Formatter\LineFormatter;
use Monolog\Handler\GelfHandler;
use Monolog\Handler\HandlerInterface;
use Monolog\Handler\StreamHandler;
use Monolog\Logger as MonologLogger;
use function array_filter;
use function array_values;

class Logger extends MonologLogger
class Logger extends MonologLogger implements Logger\SyncActionLogging, Logger\AsyncActionLogging
{
/**
* @param HandlerInterface[] $handlers
*/
public function __construct(
array $handlers = []
) {
parent::__construct('php-component');

// only keep valid handlers
$handlers = array_filter(
array_values($handlers),
function ($handler) {
return $handler instanceof HandlerInterface;
}
);

// no handlers
if (count($handlers) === 0) {
$criticalHandler = self::getDefaultCriticalHandler();
$errorHandler = self::getDefaultErrorHandler();
$logHandler = self::getDefaultLogHandler();

$handlers = [
$criticalHandler,
$errorHandler,
$logHandler,
];
}

// gelf log handler
if (count($handlers) === 1 && !($handlers[0] instanceof GelfHandler)) {
throw new \Exception(
'If only one handler is provided, it needs to be GelfHandler'
);
}

$this->setHandlers($handlers);
}

public static function getDefaultErrorHandler(): StreamHandler
{
$errorHandler = new StreamHandler('php://stderr');
Expand All @@ -62,6 +19,11 @@ public static function getDefaultErrorHandler(): StreamHandler
return $errorHandler;
}

public function __construct()
{
parent::__construct('php-component');
}

public static function getDefaultLogHandler(): StreamHandler
{
$logHandler = new StreamHandler('php://stdout');
Expand All @@ -79,4 +41,22 @@ public static function getDefaultCriticalHandler(): StreamHandler
$handler->setFormatter(new LineFormatter("[%datetime%] %level_name%: %message% %context% %extra%\n"));
return $handler;
}

public function setupSyncActionLogging(): void
{
$this->setHandlers([]);
}

public function setupAsyncActionLogging(): void
{
$criticalHandler = self::getDefaultCriticalHandler();
$errorHandler = self::getDefaultErrorHandler();
$logHandler = self::getDefaultLogHandler();

$this->setHandlers([
$criticalHandler,
$errorHandler,
$logHandler,
]);
}
}
10 changes: 10 additions & 0 deletions src/Logger/AsyncActionLogging.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Keboola\Component\Logger;

interface AsyncActionLogging
{
public function setupAsyncActionLogging(): void;
}
13 changes: 13 additions & 0 deletions src/Logger/SyncActionLogging.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Keboola\Component\Logger;

interface SyncActionLogging
{
/**
* Sync actions MUST NOT output anything to stdout
*/
public function setupSyncActionLogging(): void;
}
Loading