random improvements (#5506)

* fix order of destroying form elements
* API documentation - fixes #1949
* make command path independent
* use PhpSubprocess to execute migrations
* allow to configure optional SAML attributes
* fix deprecation
* unify wording of exported state - fixes #5392
This commit is contained in:
Kevin Papst
2025-05-28 13:34:01 +02:00
committed by GitHub
parent 81107377f4
commit 0420eb27c7
19 changed files with 150 additions and 63 deletions

View File

@@ -18,6 +18,7 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
use Symfony\Component\Process\PhpSubprocess;
/**
* Extend this class if you have a plugin that requires installation steps.
@@ -36,8 +37,7 @@ abstract class AbstractBundleInstallerCommand extends Command
}
/**
* If your bundle ships assets, that need to be available in the public/ directory,
* then overwrite this method and return: <true>.
* Return <true> if your bundle ships assets for the public/ directory.
*/
protected function hasAssets(): bool
{
@@ -189,13 +189,28 @@ abstract class AbstractBundleInstallerCommand extends Command
// prevent windows from breaking
$config = str_replace('/', DIRECTORY_SEPARATOR, $config);
$command = $this->getApplication()->find('doctrine:migrations:migrate');
$cmdInput = new ArrayInput(['--allow-no-migration' => true, '--configuration' => $config]);
$cmdInput->setInteractive(false);
if (0 !== $command->run($cmdInput, $output)) {
$process = new PhpSubprocess(
[
'bin/console',
'doctrine:migrations:migrate',
'--allow-no-migration',
'--no-interaction',
'--configuration=' . $config
],
$this->getRootDirectory()
);
$process->run();
if (!$process->isSuccessful()) {
$io->error('Failed to install bundle database: ' . PHP_EOL . $config);
$io->error($process->getErrorOutput());
throw new \Exception('Problem occurred while executing migrations.');
}
if ($io->isVerbose()) {
$io->write($process->getOutput());
}
$io->writeln('');
}
}

View File

@@ -116,7 +116,7 @@ abstract class AbstractResetCommand extends Command
}
try {
$command = $this->getApplication()->find('doctrine:query:sql');
$command = $this->getApplication()->find('dbal:run-sql');
$command->run(new ArrayInput(['sql' => 'DROP TABLE IF EXISTS migration_versions']), $output);
} catch (Exception $ex) {
$io->error('Failed to drop migration_versions table: ' . $ex->getMessage());
@@ -125,7 +125,7 @@ abstract class AbstractResetCommand extends Command
}
try {
$command = $this->getApplication()->find('doctrine:query:sql');
$command = $this->getApplication()->find('dbal:run-sql');
$command->run(new ArrayInput(['sql' => 'DROP TABLE IF EXISTS kimai2_sessions']), $output);
} catch (Exception $ex) {
$io->error('Failed to drop kimai2_sessions table: ' . $ex->getMessage());

View File

@@ -26,7 +26,8 @@ final class PluginCommand extends Command
{
public function __construct(
private readonly PluginManager $pluginManager,
private readonly PackageManager $packageManager
private readonly PackageManager $packageManager,
private readonly string $projectDirectory
)
{
parent::__construct();
@@ -69,17 +70,16 @@ final class PluginCommand extends Command
}
}
// using getApplication()->find('doctrine:migrations:migrate') does NOT work here
// because the Doctrine command can only be executed once
// if run more than once it fails with a "Container is frozen" exception
$process = new PhpSubprocess([
'bin/console',
'doctrine:migrations:migrate',
'--allow-no-migration',
'--no-interaction',
'--configuration=' . $config
]);
$process = new PhpSubprocess(
[
'bin/console',
'doctrine:migrations:migrate',
'--allow-no-migration',
'--no-interaction',
'--configuration=' . $config
],
$this->projectDirectory
);
$process->run();
if (!$process->isSuccessful()) {

View File

@@ -10,9 +10,10 @@
namespace App\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Process\PhpSubprocess;
/**
* Command used to execute all the basic application bootstrapping AFTER "composer install" was executed.
@@ -24,16 +25,36 @@ use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'kimai:reset:dev', description: 'Resets the "development" environment')]
final class ResetDevelopmentCommand extends AbstractResetCommand
{
public function __construct(string $kernelEnvironment)
public function __construct(string $kernelEnvironment, private readonly string $projectDirectory)
{
parent::__construct($kernelEnvironment);
}
protected function loadData(InputInterface $input, OutputInterface $output): void
{
$command = $this->getApplication()->find('doctrine:fixtures:load');
$cmdInput = new ArrayInput([]);
$cmdInput->setInteractive(false);
$command->run($cmdInput, $output);
$io = new SymfonyStyle($input, $output);
$io->writeln('Importing fixtures, this will take a while ... please be patient ...');
$process = new PhpSubprocess(
[
'bin/console',
'doctrine:fixtures:load',
'--no-interaction',
],
$this->projectDirectory
);
$process->run();
if (!$process->isSuccessful()) {
$io->error('Failed to load fixtures');
$io->error($process->getErrorOutput());
} else {
if ($io->isVerbose()) {
$io->write($process->getOutput());
} else {
$io->success('Fixtures loaded');
}
}
}
}

View File

@@ -27,7 +27,11 @@ final class TimesheetApiEditForm extends TimesheetEditForm
return;
}
$builder->add('billable', BillableType::class);
$builder->add('billable', BillableType::class, [
'documentation' => [
'description' => 'If true, this timesheet will be flagged as billable'
]
]);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
@@ -56,6 +60,7 @@ final class TimesheetApiEditForm extends TimesheetEditForm
$builder->remove('metaFields');
}
// TODO this is only a quick fix, see bugs reports
if ($builder->has('duration')) {
$builder->remove('duration');
}
@@ -64,11 +69,6 @@ final class TimesheetApiEditForm extends TimesheetEditForm
$builder->get('user')->setRequired(false);
}
// TODO this is only a quick fix, see bugs reports
if ($builder->has('duration')) {
$builder->remove('duration');
}
if ($builder->has('tags')) {
$builder->remove('tags');
// @deprecated for BC reasons here, arrays will be supported in 2.0
@@ -82,6 +82,10 @@ final class TimesheetApiEditForm extends TimesheetEditForm
{
$builder->add('begin', DateTimeApiType::class, array_merge($dateTimeOptions, [
'label' => 'begin',
'required' => false,
'documentation' => [
'description' => 'If no begin date-time is set, the users current timestamp will be used'
]
]));
}
@@ -90,6 +94,9 @@ final class TimesheetApiEditForm extends TimesheetEditForm
$builder->add('end', DateTimeApiType::class, array_merge($dateTimeOptions, [
'label' => 'end',
'required' => false,
'documentation' => [
'description' => 'If no end date-time is set, the timesheet will be running'
]
]));
}

View File

@@ -183,8 +183,8 @@ final class TimesheetMultiUpdate extends AbstractType
'label' => 'mark_as_exported',
'required' => false,
'choices' => [
'entryState.exported' => true,
'entryState.not_exported' => false
'yes' => true,
'no' => false
]
]);
}

View File

@@ -43,7 +43,10 @@ class TimesheetEditForm extends AbstractType
{
use FormTrait;
public function __construct(private CustomerRepository $customers, private SystemConfiguration $systemConfiguration)
public function __construct(
private readonly CustomerRepository $customers,
private readonly SystemConfiguration $systemConfiguration
)
{
}
@@ -400,14 +403,17 @@ class TimesheetEditForm extends AbstractType
}
$builder->add('exported', YesNoType::class, [
'label' => 'exported'
'label' => 'exported',
'documentation' => [
'description' => 'If true, this timesheet will be flagged as being exported'
]
]);
}
protected function addBillable(FormBuilderInterface $builder, array $options): void
{
if ($options['include_billable']) {
$builder->add('billableMode', TimesheetBillableType::class, []);
$builder->add('billableMode', TimesheetBillableType::class);
}
$builder->addModelTransformer(new CallbackTransformer(

View File

@@ -97,6 +97,9 @@ final class SamlProvider
$field = $mapping['kimai'];
$attribute = $mapping['saml'];
$value = $this->getPropertyValue($token, $attribute);
if ($value === null) {
continue;
}
$setter = 'set' . ucfirst($field);
if (method_exists($user, $setter)) {
$user->$setter($value);
@@ -117,7 +120,7 @@ final class SamlProvider
$user->setAuth(User::AUTH_SAML);
}
private function getPropertyValue(SamlLoginAttributes $token, $attribute): string
private function getPropertyValue(SamlLoginAttributes $token, $attribute): ?string
{
$results = [];
$attributes = $token->getAttributes();
@@ -129,11 +132,16 @@ final class SamlProvider
}
if ($part[0] === '$') {
$key = substr($part, 1);
if (!\array_key_exists($key, $attributes)) {
$optional = false;
if ($key[0] === '$') {
$key = substr($key, 1);
$optional = true;
}
if (!$optional && !\array_key_exists($key, $attributes)) {
throw new \RuntimeException('Missing SAML attribute in response: ' . $key);
}
if (\is_array($attributes[$key]) && isset($attributes[$key][0])) {
if (\array_key_exists($key, $attributes) && \is_array($attributes[$key]) && isset($attributes[$key][0])) {
$results[] = $attributes[$key][0];
}
} else {
@@ -145,6 +153,6 @@ final class SamlProvider
return implode(' ', $results);
}
return $attribute;
return null;
}
}