[projectId => Activity] * @var Activity[] */ protected $activities = []; /** * @var bool */ protected $debug = false; /** * @var array */ protected $oldActivities = []; /** * @param UserPasswordEncoderInterface $encoder * @param RegistryInterface $registry * @param ValidatorInterface $validator */ public function __construct( UserPasswordEncoderInterface $encoder, RegistryInterface $registry, ValidatorInterface $validator ) { $this->encoder = $encoder; $this->doctrine = $registry; $this->validator = $validator; parent::__construct(); } /** * {@inheritdoc} */ protected function configure() { $this ->setName('kimai:import-v1') ->setDescription('Import data from a Kimai v1 installation') ->setHelp('This command allows you to import the most important data from a Kimi v1 installation.') ->addArgument( 'connection', InputArgument::REQUIRED, 'The database connection as URL, e.g.: mysql://user:password@127.0.0.1:3306/kimai?charset=latin1' ) ->addArgument('prefix', InputArgument::REQUIRED, 'The database prefix for the old Kimai v1 tables') ->addArgument('password', InputArgument::REQUIRED, 'The new password for all imported user') ->addArgument('country', InputArgument::OPTIONAL, 'The default country for customer (2-character uppercase)', 'DE') ; } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { // do not convert the times, Kimai 1 stored them already in UTC Type::overrideType(Type::DATETIME, DateTimeType::class); // don't calculate rates ... this was done in Kimai 1 $this->deactivateLifecycleCallbacks($this->getDoctrine()->getConnection()); $io = new SymfonyStyle($input, $output); $config = new Configuration(); $connectionParams = ['url' => $input->getArgument('connection')]; $this->connection = DriverManager::getConnection($connectionParams, $config); $this->dbPrefix = $input->getArgument('prefix'); $password = $input->getArgument('password'); if (trim(strlen($password)) < 6) { $io->error('Password length is not sufficient, at least 6 character are required'); return; } $country = $input->getArgument('country'); if (2 != trim(strlen($country))) { $io->error('Country length needs to be exactly 2 character'); return; } if (!$this->checkDatabaseVersion($io, self::MIN_VERSION, self::MIN_REVISION)) { return; } // pre-load all data to make sure we can fully import everything $users = null; $customer = null; $projects = null; $activities = null; $records = null; $activityToProject = null; $fixedRates = null; $rates = null; $bytesStart = memory_get_usage(true); try { $users = $this->fetchAllFromImport('users'); } catch (\Exception $ex) { $io->error('Failed to load users: ' . $ex->getMessage()); return; } try { $customer = $this->fetchAllFromImport('customers'); } catch (\Exception $ex) { $io->error('Failed to load customers: ' . $ex->getMessage()); return; } try { $projects = $this->fetchAllFromImport('projects'); } catch (\Exception $ex) { $io->error('Failed to load projects: ' . $ex->getMessage()); return; } try { $activities = $this->fetchAllFromImport('activities'); } catch (\Exception $ex) { $io->error('Failed to load activities: ' . $ex->getMessage()); return; } try { $activityToProject = $this->fetchAllFromImport('projects_activities'); } catch (\Exception $ex) { $io->error('Failed to load activities-project mapping: ' . $ex->getMessage()); return; } try { $records = $this->fetchAllFromImport('timeSheet'); } catch (\Exception $ex) { $io->error('Failed to load timeSheet: ' . $ex->getMessage()); return; } try { $fixedRates = $this->fetchAllFromImport('fixedRates'); } catch (\Exception $ex) { $io->error('Failed to load fixedRates: ' . $ex->getMessage()); return; } try { $rates = $this->fetchAllFromImport('rates'); } catch (\Exception $ex) { $io->error('Failed to load rates: ' . $ex->getMessage()); return; } $bytesCached = memory_get_usage(true); $io->success('Fetched Kimai v1 data, trying to import now ...'); $allImports = 0; try { $counter = $this->importUsers($io, $password, $users, $rates); $allImports += $counter; $io->success('Imported users: ' . $counter); } catch (\Exception $ex) { $io->error('Failed to import users: ' . $ex->getMessage() . PHP_EOL . $ex->getTraceAsString()); return; } try { $counter = $this->importCustomers($io, $customer, $country); $allImports += $counter; $io->success('Imported customers: ' . $counter); } catch (\Exception $ex) { $io->error('Failed to import customers: ' . $ex->getMessage() . PHP_EOL . $ex->getTraceAsString()); return; } try { $counter = $this->importProjects($io, $projects, $fixedRates, $rates); $allImports += $counter; $io->success('Imported projects: ' . $counter); } catch (\Exception $ex) { $io->error('Failed to import projects: ' . $ex->getMessage() . PHP_EOL . $ex->getTraceAsString()); return; } try { $counter = $this->importActivities($io, $activities, $activityToProject, $fixedRates, $rates); $allImports += $counter; $io->success('Imported activities: ' . $counter); } catch (\Exception $ex) { $io->error('Failed to import activities: ' . $ex->getMessage() . PHP_EOL . $ex->getTraceAsString()); return; } try { $counter = $this->importTimesheetRecords($io, $records, $fixedRates, $rates); $allImports += $counter; $io->success('Imported timesheet records: ' . $counter); } catch (\Exception $ex) { $io->error('Failed to import timesheet records: ' . $ex->getMessage() . PHP_EOL . $ex->getTraceAsString()); return; } // TODO support expenses - new database required $bytesImported = memory_get_usage(true); $io->success( 'Memory usage: ' . PHP_EOL . 'Start: ' . $this->bytesHumanReadable($bytesStart) . PHP_EOL . 'After caching: ' . $this->bytesHumanReadable($bytesCached) . PHP_EOL . 'After import: ' . $this->bytesHumanReadable($bytesImported) . PHP_EOL . 'Total consumption for importing ' . $allImports . ' new database entries: ' . $this->bytesHumanReadable($bytesImported - $bytesStart) ); } /** * Checks if the given database connection for import has an underlying database with a compatible structure. * This is checked against the Kimai version and database revision. * * @param SymfonyStyle $io * @param string $requiredVersion * @param string $requiredRevision * @return bool * @throws \Doctrine\DBAL\DBALException */ protected function checkDatabaseVersion(SymfonyStyle $io, $requiredVersion, $requiredRevision) { $optionColumn = $this->connection->quoteIdentifier('option'); $qb = $this->connection->createQueryBuilder(); $version = $this->connection->createQueryBuilder() ->select('value') ->from($this->connection->quoteIdentifier($this->dbPrefix . 'configuration')) ->where($qb->expr()->eq($optionColumn, ':option')) ->setParameter('option', 'version') ->execute() ->fetchColumn(); $revision = $this->connection->createQueryBuilder() ->select('value') ->from($this->connection->quoteIdentifier($this->dbPrefix . 'configuration')) ->where($qb->expr()->eq($optionColumn, ':option')) ->setParameter('option', 'revision') ->execute() ->fetchColumn(); if (1 == version_compare($requiredVersion, $version)) { $io->error( 'Import can only performed from an up-to-date Kimai version:' . PHP_EOL . 'Needs at least ' . $requiredVersion . ' but found ' . $version ); return false; } if (1 == version_compare($requiredRevision, $revision)) { $io->error( 'Import can only performed from an up-to-date Kimai version:' . PHP_EOL . 'Database revision needs to be ' . $requiredRevision . ' but found ' . $revision ); return false; } return true; } /** * Remove the timesheet lifecycle events subscriber, which would overwrite values for imported timesheet records. * * @param Connection $connection */ protected function deactivateLifecycleCallbacks(Connection $connection) { $allListener = $connection->getEventManager()->getListeners(); foreach ($allListener as $name => $listener) { if (in_array($name, ['prePersist', 'preUpdate'])) { foreach ($listener as $service => $class) { if (TimesheetSubscriber::class === $class) { $connection->getEventManager()->removeEventListener(['prePersist', 'preUpdate'], $class); } } } } } /** * Thanks to "xelozz -at- gmail.com", see http://php.net/manual/en/function.memory-get-usage.php#96280 * @param $size * @return string */ protected function bytesHumanReadable($size) { $unit = ['b', 'kB', 'MB', 'GB']; $i = floor(log($size, 1024)); $a = (int) $i; return @round($size / pow(1024, $i), 2) . ' ' . $unit[$a]; } /** * @param $table * @param array $where * @return array */ protected function fetchAllFromImport($table, array $where = []) { $query = $this->connection->createQueryBuilder() ->select('*') ->from($this->connection->quoteIdentifier($this->dbPrefix . $table)); foreach ($where as $column => $value) { $query->andWhere($query->expr()->eq($column, $value)); } return $query->execute()->fetchAll(); } /** * @return RegistryInterface */ protected function getDoctrine() { return $this->doctrine; } /** * @param SymfonyStyle $io * @param $object * @return bool */ protected function validateImport(SymfonyStyle $io, $object) { $errors = $this->validator->validate($object); if ($errors->count() > 0) { /** @var \Symfony\Component\Validator\ConstraintViolation $error */ foreach ($errors as $error) { $io->error( (string) $error ); } return false; } return true; } /** * -- are currently unsupported fields that can't be mapped * * ["userID"]=> string(9) "833336177" * ["name"]=> string(5) "admin" * ["alias"]=> NULL * --- ["status"]=> string(1) "0" * ["trash"]=> string(1) "0" * ["active"]=> string(1) "1" * ["mail"]=> string(21) "foo@bar.com" * ["password"]=> string(32) "" * ["passwordResetHash"]=> NULL * ["ban"]=> string(1) "0" * ["banTime"]=> string(1) "0" * --- ["secure"]=> string(30) "" * ["lastProject"]=> string(1) "2" * ["lastActivity"]=> string(1) "2" * ["lastRecord"]=> string(1) "2" * ["timeframeBegin"]=> string(10) "1304200800" * ["timeframeEnd"]=> string(1) "0" * ["apikey"]=> NULL * ["globalRoleID"]=> string(1) "1" * * @param SymfonyStyle $io * @param string $password * @param array $users * @param array $rates * @return int * @throws \Exception */ protected function importUsers(SymfonyStyle $io, $password, $users, $rates) { $counter = 0; $entityManager = $this->getDoctrine()->getManager(); foreach ($users as $oldUser) { $isActive = (bool) $oldUser['active'] && !(bool) $oldUser['trash'] && !(bool) $oldUser['ban']; $role = (1 == $oldUser['globalRoleID']) ? User::ROLE_SUPER_ADMIN : User::DEFAULT_ROLE; $user = new User(); $user->setUsername($oldUser['name']) ->setAlias($oldUser['alias']) ->setEmail($oldUser['mail']) ->setPlainPassword($password) ->setEnabled($isActive) ->setRoles([$role]) ; $pwd = $this->encoder->encodePassword($user, $user->getPlainPassword()); $user->setPassword($pwd); if (!$this->validateImport($io, $user)) { throw new \Exception('Failed to validate user: ' . $user->getUsername()); } // find and migrate user preferences $prefsToImport = ['ui.lang' => 'language', 'timezone' => 'timezone']; $preferences = $this->fetchAllFromImport('preferences', ['userID' => $oldUser['userID']]); foreach ($preferences as $pref) { $key = $pref['option']; if (!array_key_exists($key, $prefsToImport)) { continue; } $newPref = new UserPreference(); $newPref ->setName($prefsToImport[$key]) ->setValue($pref['value']); $user->addPreference($newPref); } // find hourly rate foreach ($rates as $ratesRow) { if ($ratesRow['userID'] === $oldUser['userID'] && $ratesRow['activityID'] === null && $ratesRow['projectID'] === null) { $newPref = new UserPreference(); $newPref ->setName(UserPreference::HOURLY_RATE) ->setValue($ratesRow['rate']); $user->addPreference($newPref); } } try { $entityManager->persist($user); $entityManager->flush(); if ($this->debug) { $io->success('Created user: ' . $user->getUsername()); } ++$counter; } catch (\Exception $ex) { $io->error('Failed to create user: ' . $user->getUsername()); $io->error('Reason: ' . $ex->getMessage()); } $this->users[$oldUser['userID']] = $user; } return $counter; } /** * -- are currently unsupported fields that can't be mapped * * ["customerID"]=> string(2) "11" * ["name"]=> string(9) "Customer" * ["password"]=> NULL * ["passwordResetHash"]=> NULL * ["secure"]=> NULL * ["comment"]=> NULL * ["visible"]=> string(1) "1" * ["filter"]=> string(1) "0" * ["company"]=> string(14) "Customer Ltd." * --- ["vat"]=> string(2) "19" * ["contact"]=> string(2) "Someone" * ["street"]=> string(22) "Street name" * ["zipcode"]=> string(5) "12345" * ["city"]=> string(6) "Berlin" * ["phone"]=> NULL * ["fax"]=> NULL * ["mobile"]=> NULL * ["mail"]=> NULL * ["homepage"]=> NULL * ["trash"]=> string(1) "0" * ["timezone"]=> string(13) "Europe/Berlin" * * @param SymfonyStyle $io * @param array $customers * @param string $country * @return int * @throws \Exception */ protected function importCustomers(SymfonyStyle $io, $customers, $country) { $counter = 0; $entityManager = $this->getDoctrine()->getManager(); foreach ($customers as $oldCustomer) { $isActive = (bool) $oldCustomer['visible'] && !(bool) $oldCustomer['trash']; $name = $oldCustomer['name']; if (empty($name)) { $name = uniqid(); $io->warning('Found empty customer name, setting it to: ' . $name); } $customer = new Customer(); $customer ->setName($name) ->setComment($oldCustomer['comment']) ->setCompany($oldCustomer['company']) ->setFax($oldCustomer['fax']) ->setHomepage($oldCustomer['homepage']) ->setMobile($oldCustomer['mobile']) ->setMail($oldCustomer['mail']) ->setPhone($oldCustomer['phone']) ->setContact($oldCustomer['contact']) ->setAddress($oldCustomer['street'] . PHP_EOL . $oldCustomer['zipcode'] . ' ' . $oldCustomer['city']) ->setTimezone($oldCustomer['timezone']) ->setVisible($isActive) ->setCountry(strtoupper($country)) ; if (!$this->validateImport($io, $customer)) { throw new \Exception('Failed to validate customer: ' . $customer->getName()); } try { $entityManager->persist($customer); $entityManager->flush(); if ($this->debug) { $io->success('Created customer: ' . $customer->getName()); } ++$counter; } catch (\Exception $ex) { $io->error('Reason: ' . $ex->getMessage()); $io->error('Failed to create customer: ' . $customer->getName()); } $this->customers[$oldCustomer['customerID']] = $customer; } return $counter; } /** * -- are currently unsupported fields that can't be mapped * * ["projectID"]=> string(1) "1" * ["customerID"]=> string(1) "1" * ["name"]=> string(11) "Test" * ["comment"]=> string(0) "" * ["visible"]=> string(1) "1" * --- ["filter"]=> string(1) "0" * ["trash"]=> string(1) "1" * --- ["budget"]=> string(4) "0.00" * --- ["effort"]=> NULL * --- ["approved"]=> NULL * --- ["internal"]=> string(1) "0" * * @param SymfonyStyle $io * @param array $projects * @param array $fixedRates * @param array $rates * @return int * @throws \Exception */ protected function importProjects(SymfonyStyle $io, $projects, array $fixedRates, array $rates) { $counter = 0; $entityManager = $this->getDoctrine()->getManager(); foreach ($projects as $oldProject) { $isActive = (bool) $oldProject['visible'] && !(bool) $oldProject['trash']; $customer = $this->customers[$oldProject['customerID']]; $name = $oldProject['name']; if (empty($name)) { $name = uniqid(); $io->warning('Found empty project name, setting it to: ' . $name); } $project = new Project(); $project ->setCustomer($customer) ->setName($name) ->setComment($oldProject['comment'] ?: null) ->setVisible($isActive) ; foreach ($fixedRates as $fixedRow) { if ($fixedRow['activityID'] !== null || $fixedRow['projectID'] === null) { continue; } if ($fixedRow['projectID'] == $oldProject['projectID']) { $project->setFixedRate($fixedRow['rate']); } } foreach ($rates as $ratesRow) { if ($ratesRow['userID'] !== null || $ratesRow['activityID'] !== null || $ratesRow['projectID'] === null) { continue; } if ($ratesRow['projectID'] == $oldProject['projectID']) { $project->setHourlyRate($ratesRow['rate']); } } if (!$this->validateImport($io, $project)) { throw new \Exception('Failed to validate project: ' . $project->getName()); } try { $entityManager->persist($project); $entityManager->flush(); if ($this->debug) { $io->success('Created project: ' . $project->getName() . ' for customer: ' . $customer->getName()); } ++$counter; } catch (\Exception $ex) { $io->error('Failed to create project: ' . $project->getName()); $io->error('Reason: ' . $ex->getMessage()); } $this->projects[$oldProject['projectID']] = $project; } return $counter; } /** * -- are currently unsupported fields that can't be mapped * * $activities: * -- ["activityID"]=> string(1) "1" * ["name"]=> string(6) "Test" * ["comment"]=> string(0) "" * ["visible"]=> string(1) "1" * --- ["filter"]=> string(1) "0" * ["trash"]=> string(1) "1" * * $activityToProject * ["projectID"]=> string(1) "1" * ["activityID"]=> string(1) "1" * -- ["budget"]=> string(4) "0.00" * -- ["effort"]=> string(4) "0.00" * -- ["approved"]=> string(4) "0.00" * * @param SymfonyStyle $io * @param array $activities * @param array $activityToProject * @param array $fixedRates * @param array $rates * @return int * @throws \Exception */ protected function importActivities(SymfonyStyle $io, array $activities, array $activityToProject, array $fixedRates, array $rates) { $counter = 0; $entityManager = $this->getDoctrine()->getManager(); // remember which activity has at least one assigned project $oldActivityMapping = []; foreach ($activityToProject as $mapping) { $oldActivityMapping[$mapping['activityID']][] = $mapping['projectID']; } // create global activities foreach ($activities as $oldActivity) { $this->oldActivities[$oldActivity['activityID']] = $oldActivity; if (isset($oldActivityMapping[$oldActivity['activityID']])) { continue; } $this->createActivity($io, $entityManager, $oldActivity, $fixedRates, $rates, null); ++$counter; } $io->success('Created global activities: ' . $counter); // create project specific activities foreach ($activities as $oldActivity) { if (!isset($oldActivityMapping[$oldActivity['activityID']])) { continue; } foreach ($oldActivityMapping[$oldActivity['activityID']] as $projectId) { if (!isset($this->projects[$projectId])) { throw new \Exception( 'Invalid project linked to activity ' . $oldActivity['name'] . ': ' . $projectId ); } $project = $this->projects[$projectId]; $this->createActivity($io, $entityManager, $oldActivity, $fixedRates, $rates, $project); ++$counter; } } return $counter; } /** * @param SymfonyStyle $io * @param ObjectManager $entityManager * @param array $oldActivity * @param array $fixedRates * @param array $rates * @param Project $project * @return Activity * @throws \Exception */ protected function createActivity( SymfonyStyle $io, ObjectManager $entityManager, array $oldActivity, array $fixedRates, array $rates, ?Project $project ) { $activityId = $oldActivity['activityID']; $projectId = null !== $project ? $project->getId() : null; if (isset($this->activities[$activityId][$projectId])) { return $this->activities[$activityId][$projectId]; } $isActive = (bool) $oldActivity['visible'] && !(bool) $oldActivity['trash']; $name = $oldActivity['name']; if (empty($name)) { $name = uniqid(); $io->warning('Found empty activity name, setting it to: ' . $name); } $activity = new Activity(); $activity ->setName($name) ->setComment($oldActivity['comment'] ?: null) ->setVisible($isActive) ->setProject($project) ; foreach ($fixedRates as $fixedRow) { if ($fixedRow['activityID'] === null) { continue; } if ($fixedRow['projectID'] !== null && $fixedRow['projectID'] !== $projectId) { continue; } if ($fixedRow['activityID'] == $oldActivity['activityID']) { $activity->setFixedRate($fixedRow['rate']); } } foreach ($rates as $ratesRow) { if ($ratesRow['userID'] !== null || $ratesRow['activityID'] === null) { continue; } if ($ratesRow['projectID'] !== null && $ratesRow['projectID'] !== $projectId) { continue; } if ($ratesRow['activityID'] == $oldActivity['activityID']) { $activity->setHourlyRate($ratesRow['rate']); } } if (!$this->validateImport($io, $activity)) { throw new \Exception('Failed to validate activity: ' . $activity->getName()); } try { $entityManager->persist($activity); $entityManager->flush(); if ($this->debug) { $io->success('Created activity: ' . $activity->getName()); } } catch (\Exception $ex) { $io->error('Failed to create activity: ' . $activity->getName()); $io->error('Reason: ' . $ex->getMessage()); } if (!isset($this->activities[$activityId])) { $this->activities[$activityId] = []; } $this->activities[$activityId][$projectId] = $activity; return $activity; } /** * -- are currently unsupported fields that can't be mapped * * -- ["timeEntryID"]=> string(1) "1" * ["start"]=> string(10) "1306747800" * ["end"]=> string(10) "1306752300" * ["duration"]=> string(4) "4500" * ["userID"]=> string(9) "228899434" * ["projectID"]=> string(1) "1" * ["activityID"]=> string(1) "1" * ["description"]=> NULL * ["comment"]=> string(36) "a work description" * -- ["commentType"]=> string(1) "0" * ["cleared"]=> string(1) "0" * -- ["location"]=> string(0) "" * -- ["trackingNumber"]=> NULL * ["rate"]=> string(5) "50.00" * ["fixedRate"]=> string(4) "0.00" * -- ["budget"]=> NULL * -- ["approved"]=> NULL * -- ["statusID"]=> string(1) "1" * -- ["billable"]=> NULL * * @param SymfonyStyle $io * @param array $records * @param array $fixedRates * @param array $rates * @return int * @throws \Exception */ protected function importTimesheetRecords(SymfonyStyle $io, array $records, array $fixedRates, array $rates) { $counter = 0; $activityCounter = 0; $entityManager = $this->getDoctrine()->getManager(); $total = count($records); $io->writeln('Importing timesheets, please wait'); foreach ($records as $oldRecord) { $activity = null; $project = null; $activityId = $oldRecord['activityID']; $projectId = $oldRecord['projectID']; if (isset($this->projects[$projectId])) { $project = $this->projects[$projectId]; } else { $io->error('Could not create timesheet record, missing project with ID: ' . $projectId); continue; } $customerId = $project->getCustomer()->getId(); if (isset($this->activities[$activityId][$projectId])) { $activity = $this->activities[$activityId][$projectId]; } elseif (isset($this->activities[$activityId][null])) { $activity = $this->activities[$activityId][null]; } if (null === $activity && isset($this->oldActivities[$activityId])) { $oldActivity = $this->oldActivities[$activityId]; $activity = $this->createActivity($io, $entityManager, $oldActivity, $fixedRates, $rates, $project); ++$activityCounter; } if (null === $activity) { $io->error('Could not import timesheet record, missing activity with ID: ' . $activityId . '/' . $projectId . '/' . $customerId); continue; } $duration = $oldRecord['end'] - $oldRecord['start']; // FIXME create user on the fly if (!isset($this->users[$oldRecord['userID']])) { $io->error('Could not import timesheet record, unknown user: ' . $oldRecord['userID']); continue; } $timesheet = new Timesheet(); $fixedRate = $oldRecord['fixedRate']; if (!empty($fixedRate) && 0.00 != $fixedRate) { $timesheet->setFixedRate($fixedRate); } $hourlyRate = $oldRecord['rate']; if (!empty($hourlyRate) && 0.00 != $hourlyRate) { $timesheet->setHourlyRate($hourlyRate); } if ($timesheet->getFixedRate() !== null) { $timesheet->setRate($timesheet->getFixedRate()); } elseif ($timesheet->getHourlyRate() !== null) { $hourlyRate = $timesheet->getHourlyRate(); $rate = (float) $hourlyRate * ($duration / 3600); $timesheet->setRate(round($rate, 2)); } $user = $this->users[$oldRecord['userID']]; $timezone = $user->getPreferenceValue('timezone', date_default_timezone_get()); $dateTimezone = new \DateTimeZone('UTC'); $begin = new \DateTime('@' . $oldRecord['start']); $begin->setTimezone($dateTimezone); $end = new \DateTime('@' . $oldRecord['end']); $end->setTimezone($dateTimezone); // ---------- workaround for localizeDates ---------- // if getBegin() is not execute first, then the dates will we re-written in validateImport() below $timesheet->setBegin($begin)->setEnd($end)->getBegin(); // -------------------------------------------------- $timesheet ->setDescription($oldRecord['description'] ?: ($oldRecord['comment'] ?: null)) ->setUser($this->users[$oldRecord['userID']]) ->setBegin($begin) ->setEnd($end) ->setDuration($duration) ->setActivity($activity) ->setProject($project) ->setExported(intval($oldRecord['cleared']) !== 0) ->setTimezone($timezone) ; if (!$this->validateImport($io, $timesheet)) { throw new \Exception('Failed to validate timesheet record: ' . $oldRecord['timeEntryID']); } try { $entityManager->persist($timesheet); $entityManager->flush(); if ($this->debug) { $io->success('Created timesheet record: ' . $timesheet->getId()); } ++$counter; } catch (\Exception $ex) { $io->error('Failed to create timesheet record: ' . $ex->getMessage()); } $io->write('.'); if (0 == $counter % 80) { $io->writeln(' (' . $counter . '/' . $total . ')'); $entityManager->clear(Timesheet::class); } } for ($i = 0; $i < 80 - ($counter % 80); $i++) { $io->write(' '); } $io->writeln(' (' . $counter . '/' . $total . ')'); if ($activityCounter > 0) { $io->success('Created new activities during timesheet import: ' . $activityCounter); } return $counter; } }