<?php namespace DavidBadura\Taskwarrior; use DavidBadura\Taskwarrior\Config\Context; use DavidBadura\Taskwarrior\Config\Report; use DavidBadura\Taskwarrior\Exception\TaskwarriorException; use DavidBadura\Taskwarrior\Proxy\UuidContainer; use DavidBadura\Taskwarrior\Query\QueryBuilder; use DavidBadura\Taskwarrior\Serializer\Handler\CarbonHandler; use DavidBadura\Taskwarrior\Serializer\Handler\DependsHandler; use DavidBadura\Taskwarrior\Serializer\Handler\RecurringHandler; use Doctrine\Common\Collections\ArrayCollection; use JMS\Serializer\Handler\HandlerRegistryInterface; use JMS\Serializer\JsonSerializationVisitor; use JMS\Serializer\Naming\CamelCaseNamingStrategy; use JMS\Serializer\Naming\SerializedNameAnnotationStrategy; use JMS\Serializer\SerializationContext; use JMS\Serializer\Serializer; use JMS\Serializer\SerializerBuilder; use ProxyManager\Factory\LazyLoadingValueHolderFactory; use ProxyManager\Proxy\LazyLoadingInterface; use ProxyManager\Proxy\ValueHolderInterface; /** * @author David Badura <d.a.badura@gmail.com> * @author Tobias Olry <tobias.olry@gmail.com> */ class TaskManager { const PATTERN = '/^[\wäüö\.]*$/i'; /** * @var Taskwarrior */ private $taskwarrior; /** * @var Task[] */ private $tasks = []; /** * @var Serializer */ private $serializer; /** * @param Taskwarrior $taskwarrior */ public function __construct(Taskwarrior $taskwarrior) { $this->taskwarrior = $taskwarrior; } /** * @return Taskwarrior */ public function getTaskwarrior() { return $this->taskwarrior; } /** * @param Task $task * @throws TaskwarriorException */ public function save(Task $task) { $errors = $this->validate($task); if ($errors) { throw new TaskwarriorException(implode(', ', $errors)); } $json = $this->serializeTask($task); $uuid = $this->taskwarrior->import($json); $this->setValue($task, 'uuid', $uuid); $this->tasks[$uuid] = $task; $this->refresh($task); } /** * @param string $uuid * @return Task * @throws TaskwarriorException */ public function find($uuid) { if (isset($this->tasks[$uuid])) { return $this->tasks[$uuid]; } $task = $this->exportOne($uuid); return $this->tasks[$uuid] = $task; } /** * @param string $filter * @return Task[]|ArrayCollection */ public function filter($filter = null) { $result = $this->export($filter); foreach ($result as $key => $task) { // not yet known? then remember it if (!isset($this->tasks[$task->getUuid()])) { $this->tasks[$task->getUuid()] = $task; continue; } // replace result entry $result[$key] = $prev = $this->tasks[$task->getUuid()]; // not proxy? update task if (!$prev instanceof LazyLoadingInterface || !$prev instanceof ValueHolderInterface) { $this->merge($prev, $task); continue; } // wrapper object is a task? skip if ($prev->getWrappedValueHolderValue() instanceof Task) { continue; } // replace proxy initializer $prev->setProxyInitializer(function (&$wrappedObject, LazyLoadingInterface $proxy) use ($task) { $proxy->setProxyInitializer(null); $wrappedObject = $task; }); } return new ArrayCollection($result); } /** * @param string|array $filter * @return Task[]|ArrayCollection */ public function filterPending($filter = null) { return $this->filter(array_merge((array)$filter, ['status:pending'])); } /** * @param Task $task */ public function delete(Task $task) { if (!$task->getUuid()) { return; } $this->taskwarrior->delete($task->getUuid()); $this->refresh($task); } /** * @param Task $task */ public function done(Task $task) { if (!$task->getUuid()) { return; } $this->taskwarrior->done($task->getUuid()); $this->refresh($task); } /** * @param Task $task */ public function start(Task $task) { if (!$task->getUuid()) { return; } $this->taskwarrior->start($task->getUuid()); $this->refresh($task); } /** * @param Task $task */ public function stop(Task $task) { if (!$task->getUuid()) { return; } $this->taskwarrior->stop($task->getUuid()); $this->refresh($task); } /** * @param Task $task */ public function reopen(Task $task) { if (!$task->getUuid()) { return; } if ($task->isPending() || $task->isWaiting() || $task->isRecurring()) { return; } $this->taskwarrior->modify([ 'status' => Task::STATUS_PENDING ], $task->getUuid()); $this->refresh($task); } /** * @param Task $task * @return array */ public function validate(Task $task) { $errors = []; if ($task->isRecurring() && !$task->getDue()) { $errors[] = 'You cannot remove the due date from a recurring task.'; } if ($task->isRecurring() && !$task->getRecurring()) { $errors[] = 'You cannot remove the recurrence from a recurring task.'; } if ($task->getRecurring() && !$task->getDue()) { $errors[] = "A recurring task must also have a 'due' date."; } if (!preg_match(static::PATTERN, $task->getProject())) { $errors[] = sprintf("Project with the name '%s' is not allowed", $task->getProject()); } foreach ($task->getTags() as $tag) { if (!preg_match(static::PATTERN, $tag)) { $errors[] = sprintf("Tag with the name '%s' is not allowed", $tag); } } return $errors; } /** * */ public function clear() { $this->tasks = []; } /** * @return QueryBuilder */ public function createQueryBuilder() { return new QueryBuilder($this); } /** * @param Report|string $report * @return Task[]|ArrayCollection * @throws Exception\ConfigException */ public function filterByReport($report) { if (!$report instanceof Report) { $report = $this->taskwarrior->config()->getReport($report); } return $this->createQueryBuilder() ->where($report->filter) ->orderBy($report->sort) ->getResult(); } /** * @param Context|string $context * @return Task[]|ArrayCollection * @throws Exception\ConfigException */ public function filterByContext($context) { if (!$context instanceof Report) { $context = $this->taskwarrior->config()->getContext($context); } return $this->createQueryBuilder() ->where($context->filter) ->getResult(); } /** * @param string $uuid * @return Task */ public function getReference($uuid) { if (isset($this->tasks[$uuid])) { return $this->tasks[$uuid]; } $factory = new LazyLoadingValueHolderFactory(); $initializer = function ( &$wrappedObject, LazyLoadingInterface $proxy, $method ) use ($uuid) { if ('getUuid' == $method) { if (!$wrappedObject) { $wrappedObject = new UuidContainer($uuid); } } else { $proxy->setProxyInitializer(null); $wrappedObject = $this->exportOne($uuid); } }; $task = $factory->createProxy('DavidBadura\Taskwarrior\Task', $initializer); return $this->tasks[$uuid] = $task; } /** * @param Task $task */ private function refresh(Task $task) { // skip refresh & initailize task if ($task instanceof LazyLoadingInterface && !$task->isProxyInitialized()) { return; } $clean = $this->exportOne($task->getUuid()); $this->merge($task, $clean); } /** * @param string|array $filter * @return Task[] */ private function export($filter = null) { $json = $this->taskwarrior->export($filter); /** @var Task[] $tasks */ $tasks = $this->getSerializer()->deserialize($json, 'array<DavidBadura\Taskwarrior\Task>', 'json'); foreach ($tasks as $task) { if (!$task->getDependencies()) { $task->setDependencies([]); } } return $tasks; } /** * @param string|array $filter * @return Task * @throws TaskwarriorException */ private function exportOne($filter) { $tasks = $this->export($filter); if (count($tasks) == 0) { throw new TaskwarriorException('task not found'); } if (count($tasks) > 1) { throw new TaskwarriorException('multiple task found'); } return $tasks[0]; } /** * @param Task $old * @param Task $new */ private function merge(Task $old, Task $new) { $this->setValue($old, 'urgency', $new->getUrgency()); $this->setValue($old, 'status', $new->getStatus()); $this->setValue($old, 'modified', $new->getModified()); $this->setValue($old, 'start', $new->getStart()); if ($new->isPending()) { // fix reopen problem $this->setValue($old, 'end', null); } else { $this->setValue($old, 'end', $new->getEnd()); } } /** * * @param Task $task * @return string */ private function serializeTask(Task $task) { $result = $this->getSerializer()->serialize($task, 'json'); return str_replace("\\/", "/", $result); } /** * @param Task $task * @param string $attr * @param mixed $value */ private function setValue(Task $task, $attr, $value) { $refClass = new \ReflectionClass('DavidBadura\Taskwarrior\Task'); $refProp = $refClass->getProperty($attr); $refProp->setAccessible(true); $refProp->setValue($task, $value); } /** * @return Serializer */ private function getSerializer() { if ($this->serializer) { return $this->serializer; } $propertyNamingStrategy = new SerializedNameAnnotationStrategy(new CamelCaseNamingStrategy()); $visitor = new JsonSerializationVisitor($propertyNamingStrategy); $visitor->setOptions(JSON_UNESCAPED_UNICODE); return $this->serializer = SerializerBuilder::create() ->setPropertyNamingStrategy($propertyNamingStrategy) ->configureHandlers(function (HandlerRegistryInterface $registry) { $registry->registerSubscribingHandler(new CarbonHandler()); $registry->registerSubscribingHandler(new RecurringHandler()); $registry->registerSubscribingHandler(new DependsHandler($this)); }) ->addDefaultHandlers() ->setSerializationVisitor('json', $visitor) ->addDefaultDeserializationVisitors() ->build(); } /** * @return self */ public static function create() { return new self(new Taskwarrior()); } }