├── LICENCE.txt ├── README.md ├── composer.json ├── phpunit.xml.dist ├── psalm.xml.dist └── src ├── Account.php ├── Activity.php ├── Auth.php ├── Category ├── File.php ├── Link.php ├── Message.php ├── Model.php ├── Notebook.php └── Project.php ├── Comment.php ├── Comment ├── File.php ├── Link.php ├── Milestone.php ├── Notebook.php └── Task.php ├── Company.php ├── Currency.php ├── Custom └── Field.php ├── Exception.php ├── Expense.php ├── Factory.php ├── File.php ├── Helper └── Str.php ├── Invoice.php ├── Link.php ├── Me.php ├── Me └── Status.php ├── Message.php ├── Message └── Reply.php ├── Milestone.php ├── Notebook.php ├── People.php ├── People └── Status.php ├── Portfolio ├── Board.php ├── Card.php └── Column.php ├── Project.php ├── Project ├── Custom │ └── Field.php ├── File.php ├── People.php ├── Rate.php └── Template.php ├── Rest ├── Client.php ├── Request │ ├── JSON.php │ ├── Model.php │ └── XML.php ├── Resource.php ├── Resource │ ├── ActionTrait.php │ ├── ArchiveTrait.php │ ├── Company │ │ └── GetByTrait.php │ ├── CompleteTrait.php │ ├── CopyAndMoveTrait.php │ ├── DestroyTrait.php │ ├── GetAllTrait.php │ ├── GetTrait.php │ ├── MarkAsReadTrait.php │ ├── Model.php │ ├── Project │ │ ├── ActionTrait.php │ │ ├── CreateTrait.php │ │ └── GetByTrait.php │ ├── ReactTrait.php │ ├── SaveTrait.php │ ├── StoreTrait.php │ ├── TagTrait.php │ ├── Task │ │ └── GetByTrait.php │ ├── UpdateTrait.php │ ├── UploadTrait.php │ └── schemas │ │ ├── companies.json │ │ ├── expenses.json │ │ ├── invoices.json │ │ ├── links.json │ │ ├── me │ │ └── status.json │ │ ├── messages.json │ │ ├── messages │ │ └── replies.json │ │ ├── milestones.json │ │ ├── notebooks.json │ │ ├── people.json │ │ ├── people │ │ └── status.json │ │ ├── portfolio │ │ ├── boards.json │ │ ├── cards.json │ │ └── columns.json │ │ ├── projects.json │ │ ├── projects │ │ ├── custom_fields.json │ │ ├── files.json │ │ ├── rates.json │ │ └── template.json │ │ ├── resource_categories.json │ │ ├── resource_comments.json │ │ ├── roles.json │ │ ├── tags.json │ │ ├── tasklists.json │ │ ├── tasks.json │ │ ├── tasks │ │ └── files.json │ │ ├── teams.json │ │ └── time_entries.json └── Response │ ├── JSON.php │ ├── Model.php │ └── XML.php ├── Role.php ├── Tag.php ├── Task.php ├── Task ├── Custom │ └── Field.php └── File.php ├── Task_List.php ├── Team.php ├── Time.php ├── Timezone.php ├── Trash.php ├── Workload.php └── helpers.php /LICENCE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Teamwork.com PHP API 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/myabakus/teamworkpm/v/stable)](https://packagist.org/packages/myabakus/teamworkpm) 4 | [![Total Downloads](https://poser.pugx.org/myabakus/teamworkpm/downloads)](https://packagist.org/packages/myabakus/teamworkpm) 5 | 6 | This library allows you to interact with the Teamwork.com API for managing projects, tasks, milestones, people, and more. It’s designed for developers looking to automate or integrate project management processes within their PHP applications. 7 | 8 | ## Installation 9 | To install the package, run the following command in your terminal: 10 | ```bash 11 | composer require myabakus/teamworkpm 12 | ``` 13 | 14 | ## Using the Api 15 | In the following example, you will see how to use the API to create a project, add a person, define a milestone, create a task list, and assign a task with time tracking: 16 | 17 | ```php 18 | require __DIR__ . '/vendor/autoload.php'; 19 | 20 | // START configuration 21 | const API_KEY = 'horse48street'; 22 | const API_URL = 'https://yourcustomdomain.com'; // only required if you have a custom domain 23 | 24 | try { 25 | // set your keys 26 | // if you do not have a custom domain: 27 | Tpm::auth(API_KEY); 28 | 29 | // if you do have a custom domain: 30 | // Tpm::auth(API_URL, API_KEY); 31 | 32 | // if you do have a need use different format: 33 | // Tpm::auth(API_URL, API_KEY, API_FORMAT); 34 | 35 | // create a project 36 | $project_id = Tpm::project()->save([ 37 | 'name' => 'This is a test project', 38 | 'description' => 'Bla, Bla, Bla', 39 | ]); 40 | 41 | // create one people and add to project 42 | $person_id = Tpm::people()->save([ 43 | 'first_name' => 'Test', 44 | 'last_name' => 'User', 45 | 'user_name' => 'test', 46 | 'email_address' => 'email@hotmail.com', 47 | 'password' => 'foo123', 48 | 'project_id' => $project_id, 49 | ]); 50 | 51 | // create a milestone 52 | $milestone_id = Tpm::milestone()->save([ 53 | 'project_id' => $project_id, 54 | 'responsible_party_ids' => $person_id, 55 | 'title' => 'Test milestone', 56 | 'description' => 'Bla, Bla, Bla', 57 | 'deadline' => date('Ymd', strtotime('+10 day')), 58 | ]); 59 | 60 | // create a task list 61 | $task_list_id = Tpm::taskList()->save([ 62 | 'project_id' => $project_id, 63 | 'milestone_id' => $milestone_id, 64 | 'name' => 'My first task list', 65 | 'description' => 'Bla, Bla', 66 | ]); 67 | 68 | // create a task 69 | $task_id = Tpm::task()->save([ 70 | 'task_list_id' => $task_list_id, 71 | 'content' => 'Test Task', 72 | 'notify' => false, 73 | 'description' => 'Bla, Bla, Bla', 74 | 'due_date' => date('Ymd', strtotime('+10 days')), 75 | 'start_date' => date('Ymd'), 76 | 'private' => false, 77 | 'priority' => 'high', 78 | 'estimated_minutes' => 1000, 79 | 'responsible_party_id' => $person_id, 80 | ]); 81 | 82 | // add time to task 83 | $time_id = Tpm::time()->save([ 84 | 'task_id' => $task_id, 85 | 'person_id' => $person_id, 86 | 'description' => 'Test Time', 87 | 'date' => date('Ymd'), 88 | 'hours' => 5, 89 | 'minutes' => 30, 90 | 'time' => '08:30', 91 | 'isbillable' => false, 92 | ]); 93 | 94 | echo 'Project Id: ' . $project_id . "\n"; 95 | echo 'Person Id: ' . $person_id . "\n"; 96 | echo 'Milestone Id: ' . $milestone_id . "\n"; 97 | echo 'Task List Id: ' . $task_list_id . "\n"; 98 | echo 'Task Id: ' . $task_id . "\n"; 99 | echo 'Time id: ' . $time_id . "\n"; 100 | } catch (Exception $e) { 101 | print_r($e); 102 | } 103 | ``` 104 | 105 | View the tests folder for more details 106 | 107 | ## Console 108 | The console provides a visual interface for interacting with the API and viewing responses or debugging. 109 | 110 | ![console](https://github.com/user-attachments/assets/041dd784-fa3d-46eb-81a7-76a1b334ebf0) 111 | 112 | Save your tests fixtures 113 | 114 | ```bash 115 | > stf(Tpm::me()->get()) 116 | ``` -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myabakus/teamworkpm", 3 | "description": "PHP wrapper for Teamwork.com API", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Loduis Madariaga", 9 | "email": "loduis@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "^8.0" 14 | }, 15 | "require-dev": { 16 | "phpunit/phpunit": "^9.6", 17 | "psy/psysh": "^0.12.4", 18 | "spatie/phpunit-snapshot-assertions": "^4.2", 19 | "symfony/var-dumper": "^6.0", 20 | "vimeo/psalm": "^5.2", 21 | "vlucas/phpdotenv": "^5.6", 22 | "zircote/swagger-php": "^4.11" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "TeamWorkPm\\": "src/" 27 | }, 28 | "files": [ 29 | "src/helpers.php" 30 | ] 31 | 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "TeamWorkPm\\Tests\\": "tests/" 36 | }, 37 | "files": [ 38 | "tests/functions.php" 39 | ] 40 | }, 41 | "extra": { 42 | "branch-alias": { 43 | "dev-master": "3.0.0-dev" 44 | } 45 | }, 46 | "config": { 47 | "sort-packages": true, 48 | "preferred-install": "dist", 49 | "platform-check": true 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | tests 19 | 20 | 21 | 22 | 24 | 25 | src 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /psalm.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/Account.php: -------------------------------------------------------------------------------- 1 | fetch('account'); 23 | } 24 | 25 | /** 26 | * The 'Authenticate' Call 27 | * 28 | * @return Response 29 | * @throws Exception 30 | */ 31 | public function authenticate(): Response 32 | { 33 | return $this->fetch('authenticate'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Activity.php: -------------------------------------------------------------------------------- 1 | fetch("tasks/$id/activity"); 27 | } 28 | 29 | /** 30 | * Delete an Activity Entry 31 | * 32 | * @param int $id 33 | * @return bool 34 | * @throws Exception 35 | */ 36 | public function delete(int $id): bool 37 | { 38 | return $this->del("activity/$id"); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Auth.php: -------------------------------------------------------------------------------- 1 | null, 13 | 'key' => null, 14 | ]; 15 | 16 | private static bool $is_subdomain = false; 17 | 18 | public static function set(string ...$args): void 19 | { 20 | $numArgs = count($args); 21 | if ($numArgs === 1) { 22 | static::$config['url'] = static::$url; 23 | static::$config['key'] = $args[0]; 24 | static::$config['url'] = Factory::account()->authenticate()->url; 25 | } elseif ($numArgs === 2) { 26 | static::$config['url'] = $url = $args[0]; 27 | static::checkSubDomain($url); 28 | if (static::$is_subdomain) { 29 | static::$config['url'] = static::$url; 30 | } 31 | static::$config['key'] = $args[1]; 32 | if (static::$is_subdomain) { 33 | $url = Factory::account()->authenticate()->url; 34 | } 35 | static::$config['url'] = $url; 36 | } 37 | } 38 | 39 | public static function get(): array 40 | { 41 | return array_values(static::$config); 42 | } 43 | 44 | private static function checkSubDomain(string $url): void 45 | { 46 | $eu_domain = strpos($url, '.eu'); 47 | 48 | if ($eu_domain !== false) { 49 | static::$url = 'https://authenticate.eu.teamwork.com/'; 50 | $url = substr($url, 0, $eu_domain); 51 | } 52 | if (!str_contains($url, '.')) { 53 | static::$is_subdomain = true; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Category/File.php: -------------------------------------------------------------------------------- 1 | getByProject($projectId); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Category/Notebook.php: -------------------------------------------------------------------------------- 1 | validateResource( 42 | $resource = $this->resource ?? $data->pull('resource_name') 43 | ); 44 | $resourceId = (int) $data->pull('resource_id'); 45 | $this->validates([ 46 | 'resource_id' => $resourceId 47 | ], true); 48 | $files = $data->pull('files'); 49 | if ($files !== null) { 50 | $data['pending_file_attachments'] = Factory::file() 51 | ->upload($files); 52 | } 53 | 54 | return $this->post( 55 | "$resource/$resourceId/$this->action", 56 | $data 57 | ); 58 | } 59 | 60 | /** 61 | * Retrieving Comments across all types 62 | * 63 | * @param array|object $params 64 | * @return Response 65 | * @throws Exception 66 | */ 67 | public function all(array|object $params = []): Response 68 | { 69 | $params = arr_obj($params); 70 | 71 | if (!$params->offsetExists('objectType') && $this->resource !== null) { 72 | $type = substr($this->resource, 0, -1); 73 | $params['objectType'] = str_replace('version', '', $type); 74 | } 75 | 76 | return parent::all($params); 77 | } 78 | 79 | /** 80 | * Retrieving Recent Comments 81 | * 82 | * @param int $resourceId 83 | * @param array|object $params 84 | * @return Response 85 | * @throws Exception 86 | */ 87 | public function getRecent(int $resourceId, array|object $params = []): Response 88 | { 89 | $params = arr_obj($params); 90 | 91 | $this->validateResource( 92 | $resource = $this->resource ?? $params->pull('resource_name') 93 | ); 94 | 95 | return $this->fetch( 96 | "$resource/$resourceId/$this->action", 97 | $params 98 | ); 99 | } 100 | 101 | protected function validateResource(?string $resource): void 102 | { 103 | if ($resource === null || !in_array($resource, [ 104 | 'fileversions', 105 | 'tasks', 106 | 'notebooks', 107 | 'links', 108 | 'milestones' 109 | ])) { 110 | throw new Exception('Required resource_name'); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Comment/File.php: -------------------------------------------------------------------------------- 1 | message = trim($errorInfo['Message']); 20 | if (isset($errorInfo['Response'])) { 21 | $this->response = $errorInfo['Response']; 22 | } 23 | if (isset($errorInfo['Headers'])) { 24 | $this->headers = $errorInfo['Headers']; 25 | } 26 | } 27 | 28 | public function getResponse() 29 | { 30 | return $this->response; 31 | } 32 | 33 | public function getHeaders() 34 | { 35 | return $this->headers; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Expense.php: -------------------------------------------------------------------------------- 1 | fetch("$this->action"); 34 | } 35 | 36 | /** 37 | * Get a Single File 38 | * 39 | * @param int $id 40 | * 41 | * @return Response 42 | * @throws Exception 43 | */ 44 | public function get(int $id): Response 45 | { 46 | return $this->fetch("$this->action/$id"); 47 | } 48 | 49 | /** 50 | * List Files on a Project 51 | * 52 | * @param int $id 53 | * 54 | * @return Response 55 | * @throws Exception 56 | */ 57 | public function getByProject(int $projectId) 58 | { 59 | return Factory::projectFile()->all($projectId); 60 | } 61 | 62 | /** 63 | * List Files on a Task 64 | * 65 | * @param int $id 66 | * 67 | * @return Response 68 | * @throws Exception 69 | */ 70 | public function getByTask(int $id) 71 | { 72 | return Factory::taskFile()->all($id); 73 | } 74 | 75 | /** 76 | * Upload a File (Classic) 77 | * 78 | * @param string|array $files 79 | * @return array 80 | * @throws Exception 81 | */ 82 | public function upload(string|array $files): array 83 | { 84 | $files = (array)$files; 85 | $pending_file_attachments = []; 86 | foreach ($files as $filename) { 87 | if (!is_file($filename)) { 88 | throw new Exception("Not file exist $filename"); 89 | } 90 | } 91 | foreach ($files as $filename) { 92 | $params = ['file' => curl_file_create($filename)]; 93 | $pending_file_attachments[] = $this->post( 94 | 'pendingfiles', 95 | $params 96 | ); 97 | } 98 | 99 | return $pending_file_attachments; 100 | } 101 | 102 | /** 103 | * Add a File to a Project 104 | * Add a File to a Task 105 | * 106 | * 107 | * @param array $data 108 | * 109 | * @return int 110 | * @throws Exception 111 | */ 112 | public function add(object|array $data): mixed 113 | { 114 | $data = arr_obj($data); 115 | $projectId = $data->pull('project_id'); 116 | $taskId = $data->pull('task_id'); 117 | $id = $data->pull('id'); 118 | $files = $data->pull('files'); 119 | if ($files !== null) { 120 | $files = is_string($files) ? (array) $files : $files->toArray(); 121 | if ($id !== null) { 122 | $files = $files[0]; 123 | } 124 | $data[ 125 | $taskId ? 126 | 'pending_file_attachments': 127 | 'pending_file_ref' 128 | ] = $this->upload($files); 129 | } 130 | if ($id !== null) { 131 | if (empty($data['pending_file_ref'])) { 132 | throw new Exception('Required field pending_file_ref'); 133 | } 134 | $params = [ 135 | 'pendingFileRef' => ((array) $data['pending_file_ref'])[0] 136 | ]; 137 | if (!empty($data->description)) { 138 | $params['description'] = $data['description']; 139 | } 140 | return $this->notUseFields() 141 | ->post("$this->action/$id", ['fileversion' => $params]); 142 | } 143 | 144 | if (!($taskId || $projectId)) { 145 | throw new Exception('Required field project_id or task_id'); 146 | } 147 | 148 | return $projectId ? 149 | Factory::projectFile()->add($projectId, $data): 150 | Factory::taskFile()->add($taskId, $data); 151 | } 152 | 153 | /** 154 | * Get a short URL for sharing a File 155 | * 156 | * @param integer $id 157 | * @return string 158 | */ 159 | public function getSharedLink(int $id): string 160 | { 161 | return $this->fetch("$this->action/$id/sharedlink")->url; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Helper/Str.php: -------------------------------------------------------------------------------- 1 | notUseFields() 27 | ->put("$this->action/$id/lineitems", [ 28 | 'lineitems' => [ 29 | 'add' => [ 30 | 'timelogs' => "$time" 31 | ] 32 | ] 33 | ]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Link.php: -------------------------------------------------------------------------------- 1 | fetch("$this->action"); 17 | } 18 | 19 | /** 20 | * Current User Summary Stats 21 | * 22 | * @return Response 23 | */ 24 | public function getStats(): Response 25 | { 26 | return $this->fetch('stats'); 27 | } 28 | 29 | /** 30 | * Get all your Running Timers 31 | * 32 | * @param int $id 33 | * @param array|object $params 34 | * @return Response 35 | * @throws Exception 36 | */ 37 | public function getTimers(): Response 38 | { 39 | return $this->fetch("$this->action/timers"); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Me/Status.php: -------------------------------------------------------------------------------- 1 | fetch("$this->action"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Message.php: -------------------------------------------------------------------------------- 1 | action"; 37 | if ($archived) { 38 | $action .= '/archive'; 39 | } 40 | return $this->fetch($action); 41 | } 42 | 43 | /** 44 | * Retrieve Messages by Category | Get Archived Messages by Category 45 | * 46 | * @param integer $projectId 47 | * @param integer $categoryId 48 | * @param boolean $archived 49 | * @return Response 50 | */ 51 | public function getByProjectAndCategory(int $projectId, int $categoryId, bool $archived = false): Response 52 | { 53 | $action = "projects/$projectId/cat/$categoryId/$this->action"; 54 | if ($archived) { 55 | $action .= '/archive'; 56 | } 57 | return $this->fetch($action); 58 | } 59 | 60 | /** 61 | * Mark a Resource as archive 62 | * 63 | * @param int $id 64 | * @return bool 65 | * @throws Exception 66 | */ 67 | public function archive(int $id): bool 68 | { 69 | return $this->put("messages/$id/archive"); 70 | } 71 | 72 | /** 73 | * Mark a Resource as unarchive 74 | * 75 | * @param int $id 76 | * @return bool 77 | * @throws Exception 78 | */ 79 | public function unArchive(int $id): bool 80 | { 81 | return $this->put("messages/$id/unarchive"); 82 | } 83 | 84 | /** 85 | * Mark a Resource as Read 86 | * 87 | * @param integer $id 88 | * @return boolean 89 | */ 90 | public function markAsRead(int $id): bool 91 | { 92 | return $this->put("messages/$id/markread"); 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /src/Message/Reply.php: -------------------------------------------------------------------------------- 1 | fetch("messages/$id/replies", $params); 41 | } 42 | 43 | /** 44 | * Create a Message Reply 45 | * 46 | * @param array|object $data 47 | * @return int 48 | */ 49 | public function create(array|object $data): int 50 | { 51 | $data = arr_obj($data); 52 | $messageId = $data->pull('message_id'); 53 | 54 | $this->validates([ 55 | 'message_id' => $messageId 56 | ], true); 57 | 58 | return $this->post("messages/$messageId/$this->action", $data); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Milestone.php: -------------------------------------------------------------------------------- 1 | put("$this->action/$id/lock"); 35 | } 36 | 37 | /** 38 | * Unlock a Single Notebook 39 | * 40 | * @param int $id 41 | * @return bool 42 | * @throws Exception 43 | */ 44 | public function unlock(int $id) 45 | { 46 | return $this->put("$this->action/$id/unlock"); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/People.php: -------------------------------------------------------------------------------- 1 | fetch("$this->action/APIKeys"); 39 | } 40 | 41 | /** 42 | * Get available People for a Calendar Event 43 | * Get available People for a Message 44 | * Get available People for a Milestone 45 | * Get available People for following a Notebook 46 | * Get available People for a Task 47 | * Get available People to notify when adding a File 48 | * Get available People to notify when adding a Link 49 | * 50 | * @param string $resource 51 | * @param object|array $params 52 | * 53 | * @return Response 54 | * @throws Exception 55 | */ 56 | public function getAvailableFor(string $resource, object|array $params = []): Response 57 | { 58 | if (!in_array($resource, [ 59 | 'calendar_events', 60 | 'messages', 61 | 'milestones', 62 | 'notebooks', 63 | 'tasks', 64 | 'files', 65 | 'links'] 66 | )) { 67 | throw new Exception("Invalid resource for available: " . $resource); 68 | } 69 | 70 | $params = arr_obj($params); 71 | 72 | [$path, $subpath, $id, $isCalendar] = match($resource) { 73 | 'calendar_events' => [ 74 | 'calendarevents', 75 | null, 76 | (int) $params->pull('event_id'), 77 | true 78 | ], 79 | default => [ 80 | 'projects', 81 | $resource, 82 | (int) $params->pull('project_id'), 83 | false 84 | ] 85 | }; 86 | 87 | if ($isCalendar) { 88 | $this->validates(['event_id' => $id]); 89 | } else { 90 | $this->validates(['project_id' => $id]); 91 | } 92 | 93 | $path .= "/$id/"; 94 | if (!$isCalendar) { 95 | $path .= "$subpath/"; 96 | } 97 | $path .= 'availablepeople'; 98 | 99 | return $this->fetch($path, $params); 100 | } 101 | 102 | 103 | /** 104 | * Get all deleted People 105 | * 106 | * @return Response 107 | * @throws Exception 108 | */ 109 | public function getDeleted(object|array $params = []): Response 110 | { 111 | return $this->fetch("$this->action/deleted", $params); 112 | } 113 | 114 | /** 115 | * Get all People (within a Project) 116 | * And 117 | * Get a Users Permissions on a Project 118 | * 119 | * @param int $id 120 | * @param ?int $personId 121 | * 122 | * @return Response 123 | * @throws Exception 124 | */ 125 | public function getByProject(int $id, ?int $personId = null): Response 126 | { 127 | $path = "projects/$id/$this->action"; 128 | if ($personId !== null) { 129 | $path .= '/' . $personId; 130 | } 131 | 132 | return $this->fetch($path); 133 | } 134 | 135 | /** 136 | * Creates a new User Account 137 | * 138 | * @param array|object $data 139 | * @return integer 140 | */ 141 | public function create(array|object $data): int 142 | { 143 | $data = arr_obj($data); 144 | $projectId = (int) $data->pull('project_id'); 145 | $permissions = $data->pull('permissions'); 146 | $id = parent::create($data); 147 | if ($projectId) { 148 | $permission = Factory::projectPeople(); 149 | if ($permission->add($projectId, $id) && $permissions !== null) { 150 | $permissions['person_id'] = $id; 151 | $permissions['project_id'] = $projectId; 152 | $permission->update($permissions); 153 | } 154 | } 155 | 156 | return $id; 157 | } 158 | 159 | /** 160 | * Editing a User 161 | * 162 | * @param array|object $data 163 | * @return boolean 164 | */ 165 | public function update(array|object $data): bool 166 | { 167 | $data = arr_obj($data); 168 | $projectId = (int) $data->pull('project_id'); 169 | $permissions = $data->pull('permissions'); 170 | $id = (int) $data->pull('id'); 171 | $save = true; 172 | if ($data->has()) { 173 | $data['id'] = $id; 174 | $save = parent::update($data); 175 | } 176 | // add permission to project 177 | if ($projectId) { 178 | $permission = Factory::projectPeople(); 179 | try { 180 | $add = $permission->add($projectId, $id); 181 | } catch (Exception $e) { 182 | $add = $e->getMessage() == 'User is already on project'; 183 | } 184 | $save = $save && $add; 185 | if ($add && $permissions !== null && $permissions) { 186 | $permissions['person_id'] = $id; 187 | $permissions['project_id'] = $projectId; 188 | $save = $permission->update($permissions); 189 | } 190 | } 191 | 192 | return $save; 193 | } 194 | 195 | /** 196 | * Get Logged Time by Person 197 | * 198 | * @param int $id 199 | * @param array|object $params 200 | * @return Response 201 | * @throws Exception 202 | */ 203 | public function getLoggedTime(int $id, array|object $params = []): Response 204 | { 205 | return $this->fetch("$this->action/$id/loggedtime", $params); 206 | } 207 | 208 | /** 209 | * Get all Running Timers for a specific Person 210 | * 211 | * @param int $id 212 | * @param array|object $params 213 | * @return Response 214 | * @throws Exception 215 | */ 216 | public function getTimers(int $id, array|object $params = []): Response 217 | { 218 | return $this->fetch("$this->action/$id/timers", $params); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/People/Status.php: -------------------------------------------------------------------------------- 1 | fetch("statuses", $params); 31 | } 32 | 33 | /** 34 | * Retrieve a Persons Status 35 | * 36 | * @param int $personId 37 | * @return Response 38 | * @throws Exception 39 | */ 40 | public function get(int $personId) 41 | { 42 | return $this->fetch($this->resolvePath($personId)); 43 | } 44 | 45 | /** 46 | * Create a User Status 47 | * 48 | * @param array|object $data 49 | * @return int 50 | * @throws Exception 51 | */ 52 | public function create(array|object $data): int 53 | { 54 | $data = arr_obj($data); 55 | 56 | $personId = $data->pull('person_id'); 57 | 58 | $this->validates([ 59 | 'person_id' => $personId 60 | ]); 61 | $path = $this->resolvePath($personId); 62 | 63 | return $this->post($path, $data); 64 | } 65 | 66 | /** 67 | * Update User Status | Update my Status 68 | * 69 | * @param array|object $data 70 | * 71 | * @return bool 72 | * @throws Exception 73 | */ 74 | public function update(array|object $data): bool 75 | { 76 | $data = arr_obj($data); 77 | $id = (int) $data->pull('id'); 78 | 79 | $this->validates([ 80 | 'id' => $id 81 | ]); 82 | 83 | $personId = $data->pull('person_id'); 84 | 85 | $path = $this->resolvePath($personId); 86 | 87 | return $this->put("$path/$id", $data); 88 | } 89 | 90 | /** 91 | * Delete a Persons Status 92 | * 93 | * @param int $id 94 | * @param ?int $personId 95 | * 96 | * @return bool 97 | * @throws Exception 98 | */ 99 | public function delete(int $id, ?int $personId = null) 100 | { 101 | $path = $this->resolvePath($personId); 102 | 103 | return $this->del("$path/$id"); 104 | } 105 | 106 | protected function resolvePath(?int $personId): string 107 | { 108 | return 'people' . 109 | ($personId !== null ? "/$personId" : '') . '/' . 110 | $this->action; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Portfolio/Board.php: -------------------------------------------------------------------------------- 1 | getByBoard($id); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Portfolio/Card.php: -------------------------------------------------------------------------------- 1 | validates([ 35 | 'column_id' => $columnId, 36 | 'project_id' => $projectId 37 | ], true); 38 | 39 | return $this->post( 40 | "portfolio/columns/$columnId/cards", compact('projectId') 41 | ); 42 | } 43 | 44 | /** 45 | * Get Cards inside a Portfolio Column 46 | * 47 | * @param integer $id 48 | * @return Response 49 | */ 50 | public function getByColumn(int $id): Response 51 | { 52 | return $this->fetch("portfolio/columns/$id/cards"); 53 | } 54 | 55 | /** 56 | * Undocumented function 57 | * 58 | * @param integer $id 59 | * @param integer $oldColumnId 60 | * @param integer $columnId 61 | * @param integer $positionAfterId 62 | * @return boolean 63 | */ 64 | public function move(int $id, int $oldColumnId, int $columnId, int $positionAfterId): bool 65 | { 66 | return $this 67 | ->notUseFields() 68 | ->put("$this->action/$id/move", compact( 69 | 'oldColumnId', 70 | 'columnId', 71 | 'positionAfterId' 72 | ) 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Portfolio/Column.php: -------------------------------------------------------------------------------- 1 | fetch("portfolio/boards/$id/columns"); 39 | } 40 | 41 | /** 42 | * Add a column to the given Board 43 | * 44 | * @param array|object $data 45 | * @return int 46 | * @throws Exception 47 | */ 48 | public function create(array|object $data): int 49 | { 50 | $data = arr_obj($data); 51 | 52 | $boardId = $data->pull('board_id'); 53 | $this->validates([ 54 | 'board_id' => $boardId 55 | ], true); 56 | 57 | return $this->post("portfolio/boards/$boardId/columns", $data); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Project.php: -------------------------------------------------------------------------------- 1 | getByStatus('all', $params); 35 | } 36 | 37 | /** 38 | * Retrieve all active projects. 39 | * 40 | * @param array $params Optional query parameters 41 | * @return Response 42 | * @throws Exception 43 | */ 44 | public function getActive(array $params = []): Response 45 | { 46 | return $this->getByStatus('active', $params); 47 | } 48 | 49 | /** 50 | * Retrieve all archived projects. 51 | * 52 | * @param array $params Optional query parameters 53 | * @return Response 54 | * @throws Exception 55 | */ 56 | public function getArchived(array $params = []): Response 57 | { 58 | return $this->getByStatus('archived', $params); 59 | } 60 | 61 | /** 62 | * Retrieves all starred projects. 63 | * 64 | * @return Response 65 | * @throws Exception 66 | */ 67 | public function getStarred(): Response 68 | { 69 | return $this->fetch("$this->action/starred"); 70 | } 71 | 72 | /** 73 | * Get Project Rates 74 | * 75 | * @param int $id 76 | * @param object|array $params 77 | * 78 | * @return Response 79 | * @throws Exception 80 | */ 81 | public function getRates(int $id, object|array $params = []): Response 82 | { 83 | return Factory::projectRate()->get($id, $params); 84 | } 85 | 86 | /** 87 | * Set Project Rates 88 | * 89 | * @param int $id 90 | * @param object|array $data 91 | 92 | * @return bool 93 | * @throws Exception 94 | */ 95 | public function setRates(int $id, object|array $data = []): bool 96 | { 97 | return Factory::projectRate()->set($id, $data); 98 | } 99 | 100 | /** 101 | * Get all People (within a Project) 102 | * 103 | * @param int $id 104 | * 105 | * @return Response 106 | * @throws Exception 107 | */ 108 | public function getPeople(int $id): Response 109 | { 110 | return Factory::people()->getByProject($id); 111 | } 112 | 113 | /** 114 | * Get all People (within a Project) 115 | * 116 | * @param int $id 117 | * 118 | * @return Response 119 | * @throws Exception 120 | */ 121 | public function getFiles(int $id): Response 122 | { 123 | return Factory::file()->getByProject($id); 124 | } 125 | 126 | /** 127 | * Retrieve all Time Entries for a Project 128 | * 129 | * @param integer $id 130 | * @return Response 131 | */ 132 | public function getTimes(int $id): Response 133 | { 134 | return Factory::time()->getByProject($id); 135 | } 136 | 137 | /** 138 | * Get Project Stats 139 | * 140 | * @return Response 141 | * @throws Exception 142 | */ 143 | public function getStats(int $id, object|array $params = []): Response 144 | { 145 | return $this->fetch("$this->action/$id/stats", $params); 146 | } 147 | 148 | /** 149 | * Time Totals on a Project 150 | * 151 | * @param int $id 152 | * @param array|object $params 153 | * @return Response 154 | * @throws Exception 155 | */ 156 | public function getTotalTime(int $id, array|object $params = []): Response 157 | { 158 | return $this->fetch("$this->action/$id/time/total", $params); 159 | } 160 | 161 | /** 162 | * Time Totals on a Project | Time Totals across Projects 163 | * 164 | * @param int $id 165 | * @param array|object $params 166 | * @return Response 167 | * @throws Exception 168 | */ 169 | public function getTotalTimes(): Response 170 | { 171 | return $this->fetch("$this->action/time/total"); 172 | } 173 | 174 | /** 175 | * Marks a project as starred. 176 | * 177 | * @param int $id Project ID 178 | * @return bool 179 | * @throws Exception 180 | */ 181 | public function star(int $id): bool 182 | { 183 | $this->validates(['id' => $id]); 184 | /** @var bool */ 185 | return $this->put("$this->action/$id/star"); 186 | } 187 | 188 | /** 189 | * Un mark a project as starred. 190 | * 191 | * @param int $id Project ID 192 | * @return bool 193 | * @throws Exception 194 | */ 195 | public function unStar(int $id): bool 196 | { 197 | $this->validates(['id' => $id]); 198 | /** @var bool */ 199 | return $this->put("$this->action/$id/unstar"); 200 | } 201 | 202 | /** 203 | * Activates a project (sets it as active). 204 | * 205 | * @param int $id Project ID 206 | * @return bool 207 | * @throws Exception 208 | */ 209 | public function activate(int $id): bool 210 | { 211 | $this->validates(['id' => $id]); 212 | 213 | return $this->update(['id' => $id, 'status' => 'active']); 214 | } 215 | 216 | /** 217 | * Archives a project. 218 | * 219 | * @param int $id Project ID 220 | * @return bool 221 | * @throws Exception 222 | */ 223 | public function archive(int $id): bool 224 | { 225 | $this->validates(['id' => $id]); 226 | 227 | return $this->update(['id' => $id, 'status' => 'archived']); 228 | } 229 | 230 | /** 231 | * Retrieves projects by their status (active, archived, all). 232 | * 233 | * @param string $status 234 | * @param object|array $params 235 | * @return Response 236 | * @throws Exception 237 | */ 238 | private function getByStatus(string $status, object|array $params = []): Response 239 | { 240 | $params = arr_obj($params); 241 | $params['status'] = strtoupper($status); 242 | 243 | return $this->fetch("$this->action", $params); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/Project/Custom/Field.php: -------------------------------------------------------------------------------- 1 | getByProject($id); 36 | } 37 | 38 | /** 39 | * Add a File to a Project 40 | * 41 | * @param integer $id 42 | * @param object|array $data 43 | * @return int 44 | * @throws Exception 45 | */ 46 | public function add(int $id, object|array $data): int 47 | { 48 | return $this->post("projects/$id/$this->action", $data); 49 | } 50 | } -------------------------------------------------------------------------------- /src/Project/People.php: -------------------------------------------------------------------------------- 1 | fields = [ 23 | 'can_view_project_update' => [ 24 | 'type' => 'boolean', 25 | 'transform' => 'camel' 26 | ], 27 | 'view_tasks_and_milestones' => [ 28 | 'type' => 'boolean', 29 | 'transform' => 'camel' 30 | ], 31 | 'can_add_milestones' => [ 32 | 'type' => 'boolean', 33 | 'transform' => 'camel' 34 | ], 35 | 'can_add_task_lists' => [ 36 | 'type' => 'boolean', 37 | 'transform' => 'camel' 38 | ], 39 | 'view_estimated_time' => [ 40 | 'type' => 'boolean', 41 | 'transform' => 'camel' 42 | ], 43 | 'can_add_tasks' => [ 44 | 'type' => 'boolean', 45 | 'transform' => 'camel' 46 | ], 47 | 'view_messages_and_files' => [ 48 | 'type' => 'boolean', 49 | 'transform' => 'camel' 50 | ], 51 | 'can_add_messages' => [ 52 | 'type' => 'boolean', 53 | 'transform' => 'camel' 54 | ], 55 | 'can_add_files' => [ 56 | 'type' => 'boolean', 57 | 'transform' => 'camel' 58 | ], 59 | 'view_time_log' => [ 60 | 'type' => 'boolean', 61 | 'transform' => 'camel' 62 | ], 63 | 'view_all_time_logs' => [ 64 | 'type' => 'boolean', 65 | 'transform' => 'camel' 66 | ], 67 | 'can_log_time' => [ 68 | 'type' => 'boolean', 69 | 'transform' => 'camel' 70 | ], 71 | 'can_view_project_budget' => [ 72 | 'type' => 'boolean', 73 | 'transform' => 'camel' 74 | ], 75 | 'view_notebook' => [ 76 | 'type' => 'boolean', 77 | 'transform' => 'camel' 78 | ], 79 | 'can_add_notebooks' => [ 80 | 'type' => 'boolean', 81 | 'transform' => 'camel' 82 | ], 83 | 'view_risk_register' => [ 84 | 'type' => 'boolean', 85 | 'transform' => 'camel' 86 | ], 87 | 'view_links' => [ 88 | 'type' => 'boolean', 89 | 'transform' => 'camel' 90 | ], 91 | 'can_add_links' => [ 92 | 'type' => 'boolean', 93 | 'transform' => 'camel' 94 | ], 95 | 'can_view_forms' => [ 96 | 'type' => 'boolean', 97 | 'transform' => 'camel' 98 | ], 99 | 'can_add_forms' => [ 100 | 'type' => 'boolean', 101 | 'transform' => 'camel' 102 | ], 103 | 'can_manage_custom_fields' => [ 104 | 'type' => 'boolean', 105 | 'transform' => 'camel' 106 | ], 107 | 'project_administrator' => [ 108 | 'type' => 'boolean', 109 | 'transform' => 'camel' 110 | ], 111 | ]; 112 | } 113 | 114 | /** 115 | * @param int $projectId 116 | * @param int $personId 117 | * 118 | * @return Response 119 | * @throws Exception 120 | */ 121 | public function get(int $projectId, int $personId) 122 | { 123 | $this->validates(compact('projectId', 'personId')); 124 | 125 | return $this->fetch("projects/$projectId/people/$personId"); 126 | } 127 | 128 | /** 129 | * @param int $projectId 130 | * @param int $personId 131 | * 132 | * @return bool 133 | * @throws Exception 134 | */ 135 | public function add(int $projectId, int $personId): bool 136 | { 137 | $this->validates(compact('projectId', 'personId')); 138 | 139 | /** @var bool */ 140 | return $this->post("projects/$projectId/people/$personId"); 141 | } 142 | 143 | /** 144 | * Update a users permissions on a project 145 | * PUT /projects/{id}/people/{id}.xml 146 | * Sets the permissions of a given user on a given project. 147 | * 148 | * @param array $data 149 | * 150 | * @return bool 151 | * @throws Exception 152 | */ 153 | public function update(object|array $data) 154 | { 155 | $data = arr_obj($data); 156 | $project_id = (int) $data->pull('project_id'); 157 | $person_id = (int) $data->pull('person_id'); 158 | $this->validates(compact('project_id', 'person_id')); 159 | 160 | /** @var bool */ 161 | return $this->put( 162 | "projects/$project_id/people/$person_id", 163 | $data 164 | ); 165 | } 166 | 167 | /** 168 | * @param int $projectId 169 | * @param int $personId 170 | * 171 | * @return mixed 172 | * @throws Exception 173 | */ 174 | public function delete(int $projectId, int $personId) 175 | { 176 | $this->validates(compact('projectId', 'personId')); 177 | 178 | return $this->del("projects/$projectId/people/$personId"); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/Project/Rate.php: -------------------------------------------------------------------------------- 1 | fetch("projects/$projectId/$this->actions", $params); 31 | } 32 | 33 | /** 34 | * @param int $projectId 35 | * @param object|array $data 36 | * 37 | * @return bool 38 | * @throws Exception 39 | */ 40 | public function set(int $projectId, object|array $data) 41 | { 42 | return $this->post("projects/$projectId/$this->actions", $data); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Project/Template.php: -------------------------------------------------------------------------------- 1 | fetch("$this->action/api/v3/projects/templates", $params); 31 | } 32 | 33 | /** 34 | * @param array|object $data 35 | * @return int 36 | */ 37 | public function create(array|object $data): int 38 | { 39 | /** 40 | * @var int 41 | */ 42 | return $this->post("$this->action/template", $data); 43 | } 44 | } -------------------------------------------------------------------------------- /src/Rest/Client.php: -------------------------------------------------------------------------------- 1 | key = $key; 52 | $this->url = $url; 53 | $format = strtoupper(self::$FORMAT); 54 | $request = '\\' . __NAMESPACE__ . '\Request\\' . $format; 55 | $response = '\\' . __NAMESPACE__ . '\Response\\' . $format; 56 | /** @psalm-suppress PropertyTypeCoercion */ 57 | $this->request = new $request(); 58 | /** @psalm-suppress PropertyTypeCoercion */ 59 | $this->response = new $response(); 60 | } 61 | 62 | /** 63 | * Call to api 64 | * 65 | * @param string $method 66 | * @param string $path 67 | * @param mixed $parameters 68 | * @return mixed 69 | * @throws Exception 70 | */ 71 | private function request(string $method, string $path, $parameters = null, array $opts = []): bool | int | string | Response 72 | { 73 | $url = "{$this->url}$path." . static::$FORMAT; 74 | $headers = [ 75 | 'Authorization: BASIC ' . base64_encode($this->key . ':xxx'), 76 | ]; 77 | $request = $this->request 78 | ->setOpts($opts) 79 | ->setAction($path); 80 | $useFiles = $request->useFiles(); 81 | 82 | $ch = static::initCurl( 83 | $method, 84 | $url, 85 | $request = $request->getParameters($method, $parameters), 86 | $headers 87 | ); 88 | 89 | $i = 0; 90 | $data = ''; 91 | $headers = []; 92 | $header_size = 0; 93 | $status = 0; 94 | while ($i < 5) { 95 | $data = (string) curl_exec($ch); 96 | $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); 97 | $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); 98 | $headers = $this->parseHeaders(substr($data, 0, $header_size)); 99 | if ($status === 400 && (int)$headers['x-ratelimit-remaining'] === 0) { 100 | $i++; 101 | $reset = $headers['x-ratelimit-reset']; 102 | sleep($reset); 103 | } else { 104 | break; 105 | } 106 | } 107 | // print_r($headers); 108 | // echo $request, PHP_EOL; 109 | // echo $data, PHP_EOL, PHP_EOL; 110 | $body = substr($data, $header_size); 111 | $errorInfo = curl_error($ch); 112 | $error = curl_errno($ch); 113 | curl_close($ch); 114 | if ($error) { 115 | throw new Exception($errorInfo); 116 | } 117 | 118 | if ($status === 204) { 119 | $body = '{}'; 120 | } 121 | 122 | $headers['Status'] = $status; 123 | $headers['Method'] = $method; 124 | $headers['X-Url'] = $url; 125 | $headers['X-Request'] = $request; 126 | $headers['X-Action'] = $path; 127 | $headers['X-Parent'] = $this->request->getParent(); 128 | $headers['X-Not-Use-Files'] = !$useFiles; 129 | // for chrome use 130 | // $headers['X-Authorization'] = 'BASIC ' . base64_encode($this->key . ':xxx'); 131 | // print_r($headers); 132 | 133 | return $this->response->parse($body, $headers); 134 | } 135 | 136 | /** 137 | * @param string $method 138 | * @param string $url 139 | * @param string $params 140 | * @param array $headers 141 | * @return CurlHandle 142 | */ 143 | private static function initCurl(string $method, string $url, null|string|array $params, array $headers): CurlHandle 144 | { 145 | $ch = curl_init(); 146 | switch ($method) { 147 | case 'GET': 148 | if ($params !== null && !empty($params)) { 149 | $url .= '?' . $params; 150 | } 151 | break; 152 | case 'DELETE': 153 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); 154 | break; 155 | case 'PUT': 156 | case 'POST': 157 | if ($method === 'POST') { 158 | curl_setopt($ch, CURLOPT_POST, true); 159 | } else { 160 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); 161 | } 162 | if ($params !== null && !empty($params)) { 163 | curl_setopt($ch, CURLOPT_POSTFIELDS, $params); 164 | } 165 | if (is_array($params) && isset($params['file']) && $params['file'] instanceof CURLFile) { 166 | $headers[] = 'Content-Type: multipart/form-data'; 167 | } else { 168 | $headers = array_merge($headers, [ 169 | 'Content-Type: application/' . self::$FORMAT, 170 | 'Content-Length:' . strlen((string) $params), 171 | ]); 172 | } 173 | break; 174 | } 175 | curl_setopt_array($ch, [ 176 | CURLOPT_HTTPHEADER => $headers, 177 | CURLOPT_URL => $url, 178 | CURLOPT_RETURNTRANSFER => true, 179 | CURLOPT_HEADER => true, 180 | CURLOPT_SSL_VERIFYHOST => false, 181 | CURLOPT_SSL_VERIFYPEER => false, 182 | ]); 183 | 184 | return $ch; 185 | } 186 | 187 | /** 188 | * Shortcut call get method to api 189 | * 190 | * @param string $path 191 | * @param object|array|null $parameters 192 | * 193 | * @return Response 194 | * @throws Exception 195 | */ 196 | public function get(string $path, object|array|null $parameters = null, array $opts = []): Response 197 | { 198 | return $this->request('GET', $path, $parameters, $opts); 199 | } 200 | 201 | public function put(string $path, object|array|null $parameters = null, array $opts = []): bool | int | Response 202 | { 203 | return $this->request('PUT', $path, $parameters, $opts); 204 | } 205 | 206 | public function post(string $path, object|array|null $parameters = null, array $opts = []): bool | int | string | Response 207 | { 208 | return $this->request('POST', $path, $parameters, $opts); 209 | } 210 | 211 | public function delete(string $path, array $opts = []): bool 212 | { 213 | return $this->request('DELETE', $path, null, $opts); 214 | } 215 | 216 | /* 217 | public function configRequest(string $parent, array $fields = []): void 218 | { 219 | $this->request->setParent($parent) 220 | ->setFields($fields); 221 | } 222 | */ 223 | 224 | /** 225 | * @return Request|null 226 | */ 227 | public function getRequest() 228 | { 229 | return $this->request; 230 | } 231 | 232 | /** 233 | * @codeCoverageIgnore 234 | */ 235 | public static function setFormat(string $value): void 236 | { 237 | static $format = ['json', 'xml']; 238 | $value = strtolower($value); 239 | if (in_array($value, $format)) { 240 | static::$FORMAT = $value; 241 | } 242 | } 243 | 244 | private function parseHeaders(string $stringHeaders): array 245 | { 246 | $headers = []; 247 | $stringHeaders = trim($stringHeaders); 248 | if ($stringHeaders) { 249 | $parts = explode("\n", $stringHeaders); 250 | foreach ($parts as $header) { 251 | $header = trim($header); 252 | if ($header && str_contains($header, ':')) { 253 | /** @psalm-suppress PossiblyUndefinedArrayOffset */ 254 | [$name, $value] = explode(':', $header, 2); 255 | $value = trim($value); 256 | $name = trim($name); 257 | if (isset($headers[$name])) { 258 | if (is_array($headers[$name])) { 259 | $headers[$name][] = $value; 260 | } else { 261 | $_val = $headers[$name]; 262 | $headers[$name] = [$_val, $value]; 263 | } 264 | } else { 265 | $headers[$name] = $value; 266 | } 267 | } 268 | } 269 | } 270 | 271 | return $headers; 272 | } 273 | 274 | public function notUseFields() 275 | { 276 | $this->request->notUseFields(); 277 | 278 | return $this; 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/Rest/Request/JSON.php: -------------------------------------------------------------------------------- 1 | useFields) { 14 | $parent = (string) $this->parent; 15 | $object->$parent = new \stdClass(); 16 | $parent = $object->$parent; 17 | $noUpdate = $this->method !== 'PUT'; 18 | foreach ($this->fields as $field => $options) { 19 | $value = $this->getValue($field, $options, $parameters); 20 | if ($value !== null && $noUpdate && 21 | ($options['on_update'] ?? false) !== false 22 | ) { 23 | continue; 24 | } 25 | if (is_string($value)) { 26 | $value = $this->decodeNumericEntities($value); 27 | } 28 | if ($value !== null) { 29 | !empty($options['sibling']) 30 | ? $object->$field = $value 31 | : $parent->$field = $value; 32 | } 33 | } 34 | $parent = (string) $this->parent; 35 | if (!count((array) $object->$parent)) { 36 | unset($object->$parent); 37 | } 38 | } else { 39 | foreach ($parameters as $key => $value) { 40 | $object->$key = $value; 41 | } 42 | } 43 | 44 | $parameters = json_encode($object); 45 | $parameters = $this->decodeNumericEntities($parameters); 46 | 47 | return $parameters; 48 | } 49 | 50 | 51 | private function decodeNumericEntities(string $text): string 52 | { 53 | return mb_decode_numericentity($text, [0x80, 0xffff, 0, 0xffff], 'UTF-8'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Rest/Request/Model.php: -------------------------------------------------------------------------------- 1 | parent = $parent; 29 | return $this; 30 | } 31 | 32 | /** 33 | * @param string $action 34 | * @return $this 35 | */ 36 | public function setAction(string $action) 37 | { 38 | $this->action = $action; 39 | return $this; 40 | } 41 | 42 | /** 43 | * @param array $fields 44 | * @return $this 45 | */ 46 | public function setFields(array $fields): static 47 | { 48 | $this->fields = $fields; 49 | return $this; 50 | } 51 | 52 | public function setOpts(array $opts): static 53 | { 54 | foreach ($opts as $key => $value) { 55 | $this->{'set'. $key}($value); 56 | } 57 | 58 | return $this; 59 | } 60 | 61 | /** 62 | * @param string $field 63 | * @param bool|array $options 64 | * @param array $parameters 65 | * 66 | * @return mixed|null 67 | * @throws Exception 68 | */ 69 | protected function getValue(string &$field, bool|array &$options, array $parameters) 70 | { 71 | if (!is_array($options)) { 72 | $options = ['required' => $options, 'attributes' => []]; 73 | } 74 | $type = $options['type'] ?? 'string'; 75 | $originalField = $field; 76 | $value = $parameters[$field] ?? null; 77 | if (($transform =$options['transform'] ?? null) !== null) { 78 | $checkValue = !array_key_exists($field, $parameters); 79 | $this->transformField($field, $transform); 80 | if ($checkValue) { 81 | $value = $parameters[$field] ?? null; 82 | } 83 | } 84 | 85 | $value = $this->coerceValue($value, $type); 86 | 87 | $this->validate($originalField, $value, $options); 88 | 89 | if ($type === 'object') { 90 | $value = $this->handleObjectType($value, $options); 91 | } elseif ($type === 'array' && 92 | is_array($value) && 93 | isset($options['properties']) 94 | ) { 95 | $value = $this->handleArrayOfObjectType($value, $options); 96 | } elseif ($value !== null && in_array($type, ['string|array', 'string|array'])) { 97 | if (is_array($value)) { 98 | $value = implode(',', $value); 99 | } else { 100 | $value = (string) $value; 101 | } 102 | } 103 | 104 | return $value; 105 | } 106 | 107 | private function handleArrayOfObjectType(mixed $value, $options) 108 | { 109 | $accept = $options['accept'] ?? null; 110 | if ($accept && 111 | preg_match('/<(.+?),(.+?)>/', $accept, $matches) && 112 | !array_is_list((array) $value) 113 | ) { 114 | $idField = $matches[1]; 115 | $valueField = $matches[2]; 116 | $processedValue = []; 117 | $indexByKey = $idField === '$key'; 118 | if ($indexByKey) { 119 | foreach ($value as $key => $val) { 120 | $processedValue[$key] = [ 121 | $valueField => is_scalar($val) ? $val : ((array)$val)[$valueField] 122 | ]; 123 | } 124 | return $processedValue; 125 | } 126 | 127 | foreach ($value as $key => $val) { 128 | $processedValue[] = [ 129 | $idField => $key, 130 | $valueField => $val 131 | ]; 132 | } 133 | $value = $processedValue; 134 | } 135 | 136 | $_options = $options['properties']; 137 | 138 | foreach ($value as $i => $entry) { 139 | $res = []; 140 | foreach ($_options as $key => $opts) { 141 | $val = $this->getValue($key, $opts, $entry); 142 | if ($val !== null) { 143 | $res[$key] = $val; 144 | } 145 | } 146 | $value[$i] = $res; 147 | } 148 | 149 | return $value; 150 | } 151 | 152 | private function handleObjectType(mixed $value, $options) 153 | { 154 | $accept = $options['accept'] ?? null; 155 | if ($accept && preg_match('/^<(\w+)>$/', $accept, $matches) && (is_arr_obj($value)) && array_is_list((array) $value)) { 156 | $wrapField = $matches[1]; 157 | if (isset($options[$wrapField])) { 158 | $value = [$wrapField => $value]; 159 | if (isset($options[$wrapField]['properties'])) { 160 | $fieldOpts = $options[$wrapField]; 161 | $fieldValue = $value[$wrapField]; 162 | $fieldValue = $this->getValue($wrapField, $fieldOpts, $value); 163 | $value[$wrapField] = $fieldValue; 164 | } 165 | } 166 | } elseif (is_object($value) || ( 167 | is_array($value) && ($value === [] || !array_is_list($value))) 168 | ) { 169 | $res = []; 170 | foreach ($options as $key => $opts) { 171 | if (is_scalar($opts)) { 172 | continue; 173 | } 174 | $val = $this->getValue($key, $opts, $value); 175 | if ($val !== null) { 176 | $res[$key] = $val; 177 | } 178 | } 179 | $value = $res; 180 | } 181 | 182 | return $value; 183 | } 184 | 185 | private function coerceValue($value, string $type) 186 | { 187 | if ($value === null) { 188 | return null; 189 | } 190 | 191 | if (is_scalar($value)) { 192 | if ($type === 'array') { 193 | return (array) $value; 194 | } elseif (!str_contains($type, '<') && !in_array($type, ['email', 'url', 'country'])) { 195 | settype($value, $type); 196 | } 197 | } 198 | 199 | return $value; 200 | } 201 | 202 | protected function validate(string $field, mixed $value, array $options): void 203 | { 204 | $isNull = $value === null; 205 | if ($this->method === 'POST' && ( 206 | $options['required'] ?? false 207 | ) !== false && $isNull 208 | ) { 209 | throw new Exception('Required field ' . $field); 210 | } 211 | // checking fields that must meet certain values 212 | if (!$isNull && ( 213 | ( 214 | $options['type'] == 'string|array' && 215 | !$this->validateStringArrayOfInteger($value, $options) 216 | ) || ( 217 | $options['type'] == 'string|array' && 218 | !$this->validateStringArrayOfString($value, $options) 219 | ) || 220 | ( 221 | !str_contains($options['type'], '|') && 222 | isset($options['validate']) && 223 | is_array($options['validate']) && 224 | !in_array($value, $options['validate']) 225 | ) || ( 226 | ($options['type'] ?? 'string') === 'email' && 227 | !filter_var($value, FILTER_VALIDATE_EMAIL) 228 | ) || ( 229 | ($options['type'] ?? 'string') === 'url' && 230 | !filter_var($value, FILTER_VALIDATE_URL) 231 | ) || ( 232 | ($options['type'] ?? 'string') === 'country' && 233 | mb_strlen($value) !== 2 234 | ) 235 | )) { 236 | throw new Exception( 237 | 'Invalid value for field ' . 238 | $field 239 | ); 240 | } 241 | } 242 | 243 | 244 | public function getParent(): string 245 | { 246 | return (string) $this->parent; 247 | } 248 | 249 | /** 250 | * Return params 251 | * 252 | * @param string $method 253 | * @param array|object|null $parameters 254 | * @return null|string|array 255 | */ 256 | public function getParameters(string $method, object|array|null $parameters): null|string|array 257 | { 258 | $result = null; 259 | 260 | $this->method = $method; 261 | 262 | if ($parameters === null) { 263 | return null; 264 | } 265 | 266 | if ($method === 'POST' && 267 | is_array($parameters) && 268 | isset($parameters['file']) && 269 | $parameters['file'] instanceof \CURLFile 270 | ) { 271 | $this->useFields = true; 272 | return $parameters; 273 | } 274 | 275 | if ($isArrayObject = is_arr_obj($parameters)) { 276 | $parameters = arr_obj($parameters)->toArray(); 277 | } 278 | 279 | if ($method === 'GET') { 280 | if ($isArrayObject) { 281 | $result = http_build_query($parameters); 282 | } 283 | } elseif ($isArrayObject) { 284 | /** @psalm-suppress PossiblyInvalidArgument */ 285 | $result = $this->parseParameters($parameters); 286 | $this->useFields = true; 287 | } 288 | 289 | return $result; 290 | } 291 | 292 | private function transformField(&$field, $fieldTransform) 293 | { 294 | $field = in_array($fieldTransform, ['camel', 'dash']) ? 295 | Str::{$fieldTransform}($field) : 296 | $fieldTransform; 297 | } 298 | 299 | private function validateStringArrayOfString(string|array $value): bool 300 | { 301 | if (is_string($value)) { 302 | if (str_contains($value, ',')) { 303 | $ids = explode(',', $value); 304 | $ids = array_map(fn($val)=> trim($val), $ids); 305 | return $this->isArrayOfStrings($ids); 306 | } 307 | return true; 308 | } 309 | 310 | return $this->isArrayOfStrings($value); 311 | } 312 | 313 | private function validateStringArrayOfInteger(int|string|array $value, array $options): bool 314 | { 315 | if (is_string($value)) { 316 | if (in_array($value, $options['validate'])) { 317 | return true; 318 | } 319 | if (str_contains($value, ',') || ctype_digit($value)) { 320 | $ids = explode(',', $value); 321 | $ids = array_map(fn($val)=> trim($val), $ids); 322 | return $this->isArrayOfIntegers($ids); 323 | } 324 | return false; 325 | } 326 | if (is_int($value)) { 327 | return true; 328 | } 329 | 330 | return $this->isArrayOfIntegers($value); 331 | } 332 | 333 | private function isArrayOfIntegers($array): bool 334 | { 335 | return is_array_of_int($array); 336 | } 337 | 338 | private function isArrayOfStrings($array): bool 339 | { 340 | if (!is_array($array)) { 341 | return false; 342 | } 343 | 344 | foreach ($array as $value) { 345 | if (!is_scalar($value) || !is_string($value)) { 346 | return false; 347 | } 348 | } 349 | 350 | return true; 351 | } 352 | 353 | public function notUseFields() 354 | { 355 | $this->useFields = false; 356 | } 357 | 358 | public function useFiles() 359 | { 360 | return $this->useFields; 361 | } 362 | 363 | /** 364 | * Return parameters for post and put request 365 | * 366 | * @param array $parameters 367 | * @return string 368 | */ 369 | abstract protected function parseParameters(array $parameters): ?string; 370 | } 371 | -------------------------------------------------------------------------------- /src/Rest/Request/XML.php: -------------------------------------------------------------------------------- 1 | doc = new DOMDocument(); 19 | $this->doc->formatOutput = true; 20 | } 21 | 22 | protected function parseParameters(array $parameters): ?string 23 | { 24 | if (!empty($parameters)) { 25 | $wrapper = $this->doc->createElement($this->getWrapper()); 26 | $parent = $this->doc->createElement($this->getParent()); 27 | if ($this->actionInclude('/reorder')) { 28 | $parent->setAttribute('type', 'array'); 29 | foreach ($parameters as $id) { 30 | $element = $this->doc->createElement((string) $this->parent); 31 | $item = $this->doc->createElement('id'); 32 | $item->appendChild($this->doc->createTextNode($id)); 33 | $element->appendChild($item); 34 | $parent->appendChild($element); 35 | } 36 | } else { 37 | $noUpdate = $this->method !== 'PUT'; 38 | foreach ($this->fields as $field => $options) { 39 | $value = $this->getValue($field, $options, $parameters); 40 | if ($value !== null && $noUpdate && 41 | ($options['on_update'] ?? false) !== false 42 | ) { 43 | continue; 44 | } 45 | $element = $this->doc->createElement($field); 46 | if (isset($options['attributes'])) { 47 | foreach ($options['attributes'] as $name => $type) { 48 | if ($value !== null) { 49 | $element->setAttribute($name, $type); 50 | if ($name == 'type') { 51 | if ($type == 'array') { 52 | $internal = $this->doc->createElement($options['element']); 53 | foreach ($value as $v) { 54 | $internal->appendChild($this->doc->createTextNode($v)); 55 | $element->appendChild($internal); 56 | } 57 | } elseif (!in_array($type, ['email'])) { 58 | settype($value, $type); 59 | } 60 | } 61 | } 62 | } 63 | } 64 | if ($value !== null) { 65 | if (is_bool($value)) { 66 | $value = var_export($value, true); 67 | } 68 | $element->appendChild($this->doc->createTextNode($value)); 69 | !empty($options['sibling']) 70 | ? $wrapper->appendChild($element) 71 | : $parent->appendChild($element); 72 | } 73 | } 74 | } 75 | $wrapper->appendChild($parent); 76 | $this->doc->appendChild($wrapper); 77 | 78 | $parameters = $this->doc->saveXML(); 79 | } else { 80 | $parameters = null; 81 | } 82 | 83 | return $parameters; 84 | } 85 | 86 | protected function getWrapper(): string 87 | { 88 | return 'request'; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Rest/Resource.php: -------------------------------------------------------------------------------- 1 | rest = $httpClient; 55 | /* 56 | if ($this->parent === null) { 57 | $this->parent = strtolower(str_replace( 58 | ['TeamWorkPm\\', '\\'], 59 | ['', '-'], 60 | static::class 61 | )); 62 | } 63 | */ 64 | $this->init(); 65 | /* 66 | if (null === $this->action) { 67 | $this->action = str_replace('-', '_', $this->parent); 68 | // pluralize 69 | if (substr($this->action, -1) === 'y') { 70 | $this->action = substr($this->action, 0, -1) . 'ies'; 71 | } else { 72 | $this->action .= 's'; 73 | } 74 | } 75 | */ 76 | if (is_string($this->fields)) { 77 | $fields = str_replace('.', DIRECTORY_SEPARATOR, $this->fields); 78 | $content = file_get_contents( 79 | implode( 80 | DIRECTORY_SEPARATOR, 81 | [ 82 | __DIR__, 83 | 'Resource', 84 | 'schemas', 85 | $fields . '.json' 86 | ] 87 | ), 88 | /* 89 | __DIR__ . DIRECTORY_SEPARATOR . '..' . 90 | DIRECTORY_SEPARATOR . 'schemas' . DIRECTORY_SEPARATOR . $fields . '.json' 91 | */ 92 | ); 93 | 94 | $this->fields = json_decode($content, true); 95 | } 96 | // configure request for put and post fields 97 | // $this->resetOptions(); 98 | } 99 | 100 | /* 101 | protected function resetOptions() 102 | { 103 | $this->rest->configRequest( 104 | $this->parent, 105 | $this->fields 106 | ); 107 | 108 | return $this; 109 | } 110 | */ 111 | 112 | /** 113 | * @codeCoverageIgnore 114 | */ 115 | final protected function __clone() 116 | { 117 | } 118 | 119 | protected function init() 120 | { 121 | } 122 | 123 | public function __call($name, $arguments) 124 | { 125 | if ($name === 'all' && method_exists($this, 'getAll')) { 126 | return $this->getAll(...$arguments); 127 | } 128 | if ($name === 'getAll' && method_exists($this, 'all')) { 129 | return $this->all(...$arguments); 130 | } 131 | if ($name === 'create' && method_exists($this, 'insert')) { 132 | return $this->insert(...$arguments); 133 | } 134 | 135 | if ($name === 'insert' && method_exists($this, 'create')) { 136 | return $this->create(...$arguments); 137 | } 138 | 139 | if ($name === 'first') { 140 | $entries = []; 141 | if (method_exists($this, 'getAll')) { 142 | $entries = $this->getAll(...$arguments); 143 | } elseif (method_exists($this, 'all')) { 144 | $entries = $this->all(...$arguments); 145 | } 146 | 147 | if (count($entries) > 0) { 148 | return $entries[0]; 149 | } 150 | 151 | return $entries; 152 | } 153 | 154 | throw new BadMethodCallException("No exists method: $name"); 155 | } 156 | 157 | protected function validates(array $ids, bool $required = false): void 158 | { 159 | $template = $required ? 'Required field' : 'Invalid param'; 160 | foreach ($ids as $field => $id) { 161 | $id = (int) $id; 162 | if ($id <= 0) { 163 | throw new Exception("$template $field"); 164 | } 165 | } 166 | } 167 | 168 | protected function put(string $path, object|array|null $parameters = null): bool | int | Response 169 | { 170 | // $this->resetOptions(); 171 | 172 | return $this->rest->put($path, $parameters, [ 173 | 'Parent' => $this->parent, 174 | 'Fields' => $this->fields 175 | ]); 176 | } 177 | 178 | protected function post(string $path, object|array|null $parameters = null): bool | int | string | Response 179 | { 180 | // $this->resetOptions(); 181 | 182 | return $this->rest->post($path, $parameters, [ 183 | 'Parent' => $this->parent, 184 | 'Fields' => $this->fields 185 | ]); 186 | } 187 | 188 | protected function fetch(string $path, object|array|null $parameters = null): Response 189 | { 190 | // $this->resetOptions(); 191 | 192 | return $this->rest->get($path, $parameters, [ 193 | 'Parent' => $this->parent, 194 | 'Fields' => [] 195 | ]); 196 | } 197 | 198 | protected function del(string $path): bool 199 | { 200 | // $this->resetOptions(); 201 | 202 | return $this->rest->delete($path, [ 203 | 'Parent' => $this->parent, 204 | 'Fields' => [] 205 | ]); 206 | } 207 | 208 | protected function notUseFields() 209 | { 210 | $this->rest->notUseFields(); 211 | 212 | return $this; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/Rest/Resource/ActionTrait.php: -------------------------------------------------------------------------------- 1 | put("$this->action/$id/archive"); 19 | } 20 | 21 | /** 22 | * Mark a Resource as unarchive 23 | * 24 | * @param int $id 25 | * @return bool 26 | * @throws Exception 27 | */ 28 | public function unArchive(int $id): bool 29 | { 30 | return $this->put("$this->action/$id/unarchive"); 31 | } 32 | } -------------------------------------------------------------------------------- /src/Rest/Resource/Company/GetByTrait.php: -------------------------------------------------------------------------------- 1 | fetch("companies/$id/$this->action", $params); 20 | } 21 | } -------------------------------------------------------------------------------- /src/Rest/Resource/CompleteTrait.php: -------------------------------------------------------------------------------- 1 | put("$this->action/$id/complete"); 19 | } 20 | 21 | /** 22 | * Mark an Item Uncomplete 23 | * 24 | * @param int $id 25 | * @return bool 26 | * @throws Exception 27 | */ 28 | public function unComplete(int $id): bool 29 | { 30 | return $this->put("$this->action/$id/uncomplete"); 31 | } 32 | } -------------------------------------------------------------------------------- /src/Rest/Resource/CopyAndMoveTrait.php: -------------------------------------------------------------------------------- 1 | notUseFields() 20 | ->put( 21 | "$this->action/$id/copy", compact('projectId') 22 | ); 23 | } 24 | 25 | /** 26 | * Move a resource to another Project 27 | * 28 | * @param integer $id 29 | * @param integer $projectId 30 | * @return boolean 31 | */ 32 | public function move(int $id, int $projectId): bool 33 | { 34 | return $this 35 | ->notUseFields() 36 | ->put("$this->action/$id/move", compact('projectId') 37 | ); 38 | } 39 | } -------------------------------------------------------------------------------- /src/Rest/Resource/DestroyTrait.php: -------------------------------------------------------------------------------- 1 | validates(['id' => $id]); 16 | 17 | return $this->del("$this->action/$id"); 18 | } 19 | } -------------------------------------------------------------------------------- /src/Rest/Resource/GetAllTrait.php: -------------------------------------------------------------------------------- 1 | fetch("$this->action", $params); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Rest/Resource/GetTrait.php: -------------------------------------------------------------------------------- 1 | validates(['id' => $id]); 19 | 20 | return $this->fetch("$this->action/$id", $params); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Rest/Resource/MarkAsReadTrait.php: -------------------------------------------------------------------------------- 1 | put("$this->action/$id/markread"); 16 | } 17 | } -------------------------------------------------------------------------------- /src/Rest/Resource/Model.php: -------------------------------------------------------------------------------- 1 | pull('project_id'); 26 | $this->validates([ 27 | 'project_id' => $projectId 28 | ], true); 29 | 30 | $this->uploadFiles($data); 31 | 32 | return $this->post("projects/$projectId/$this->action", $data); 33 | } 34 | } -------------------------------------------------------------------------------- /src/Rest/Resource/Project/GetByTrait.php: -------------------------------------------------------------------------------- 1 | fetch("projects/$id/$this->action", $params); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Rest/Resource/ReactTrait.php: -------------------------------------------------------------------------------- 1 | validateReact($data); 19 | 20 | return $this->notUseFields() 21 | ->put("$this->action/$id/react", $data); 22 | } 23 | 24 | /** 25 | * Un react to a Resource 26 | * 27 | * @param integer $id 28 | * @return boolean 29 | */ 30 | public function unReact(int $id, array|object $data = []): bool | Response 31 | { 32 | $data = $this->validateReact($data); 33 | 34 | return $this->notUseFields() 35 | ->put("$this->action/$id/unreact", $data); 36 | } 37 | 38 | private function validateReact($data) 39 | { 40 | $data = arr_obj($data); 41 | $type = $data->pull('type'); 42 | /** @disregard */ 43 | $reactionType = $data->reactionType ?? $type; 44 | if ($reactionType && !in_array($reactionType, ['heart', 'like', 'dislike', 'joy', 'frown'])) { 45 | throw new Exception("Invalid field reactionType: " . $reactionType); 46 | } 47 | /** @disregard */ 48 | if ($data->get === true) { 49 | /** @disregard */ 50 | $data->get = 'reactions'; 51 | } 52 | 53 | if ($type) { 54 | /** @disregard */ 55 | $data->reactionType = $type; 56 | } 57 | 58 | return $data; 59 | } 60 | } -------------------------------------------------------------------------------- /src/Rest/Resource/SaveTrait.php: -------------------------------------------------------------------------------- 1 | offsetExists('id') || ( 19 | $this->parent && 20 | $data->offsetExists($this->parent) && 21 | ($entry = $data[$this->parent]) && 22 | $entry->offsetExists('id') 23 | ) 24 | ) 25 | ? $this->update($data) 26 | : $this->create($data); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Rest/Resource/StoreTrait.php: -------------------------------------------------------------------------------- 1 | post((string) $this->action, $data); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Rest/Resource/TagTrait.php: -------------------------------------------------------------------------------- 1 | addTo("$this->action", $id, $data); 22 | } 23 | 24 | /** 25 | * Remove Tags for a Resource 26 | * 27 | * @param integer $id 28 | * @param integer|string|array $data 29 | * @return boolean 30 | */ 31 | public function removeTag(int $id, int|string|array $data): bool 32 | { 33 | return Factory::tag()->removeTo("$this->action", $id, $data); 34 | } 35 | 36 | /** 37 | * List All Tags for a Resource 38 | * 39 | * @param integer $id 40 | * @return Response 41 | */ 42 | public function getTags(int $id): Response 43 | { 44 | return Factory::tag()->allFor("$this->action", $id); 45 | } 46 | } -------------------------------------------------------------------------------- /src/Rest/Resource/Task/GetByTrait.php: -------------------------------------------------------------------------------- 1 | fetch("tasks/$id/$this->action", $params); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Rest/Resource/UpdateTrait.php: -------------------------------------------------------------------------------- 1 | pull('id'); 19 | if ($id <= 0) { 20 | if ($this->parent && $data->offsetExists($this->parent)) { 21 | $entry = $data[$this->parent]; 22 | $id = (int) $entry->pull('id'); 23 | } 24 | $this->validates([ 25 | 'id' => $id 26 | ], true); 27 | } 28 | 29 | /** @var bool */ 30 | return $this->put("$this->action/$id", $data); 31 | } 32 | } -------------------------------------------------------------------------------- /src/Rest/Resource/UploadTrait.php: -------------------------------------------------------------------------------- 1 | pull('files'); 15 | if ($files !== null) { 16 | $data['pending_file_attachments'] = Factory::file() 17 | ->upload($files); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/companies.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "type": "string", 4 | "required": true 5 | }, 6 | "address_one": { 7 | "type": "string" 8 | }, 9 | "address_two": { 10 | "type": "string" 11 | }, 12 | "zip": { 13 | "type": "string" 14 | }, 15 | "city": { 16 | "type": "string" 17 | }, 18 | "state": { 19 | "type": "string" 20 | }, 21 | "country_code": { 22 | "type": "country", 23 | "transform": "countrycode" 24 | }, 25 | "phone": { 26 | "type": "string" 27 | }, 28 | "fax": { 29 | "type": "string" 30 | }, 31 | "email_one": { 32 | "type": "email" 33 | }, 34 | "email_two": { 35 | "type": "email" 36 | }, 37 | "email_three": { 38 | "type": "email" 39 | }, 40 | "website": { 41 | "type": "url" 42 | }, 43 | "cid": { 44 | "type": "string", 45 | "on_update": true 46 | }, 47 | "industry_cat_id": { 48 | "type": "string", 49 | "transform": "camel", 50 | "on_update": true 51 | }, 52 | "tag_ids": { 53 | "type": "string", 54 | "transform": "camel", 55 | "on_update": true 56 | }, 57 | "logo_pending_file_ref": { 58 | "type": "string", 59 | "transform": "camel", 60 | "on_update": true 61 | }, 62 | "remove_logo": { 63 | "type": "boolean", 64 | "transform": "camel", 65 | "on_update": true 66 | }, 67 | "profile": { 68 | "type": "string", 69 | "on_update": true 70 | }, 71 | "private_notes": { 72 | "type": "string", 73 | "transform": "camel", 74 | "on_update": true 75 | } 76 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/expenses.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "type": "string", 4 | "required": true 5 | }, 6 | "project_id": { 7 | "type": "string", 8 | "transform": "dash", 9 | "required": true 10 | }, 11 | "description": { 12 | "type": "string" 13 | }, 14 | "date": { 15 | "type": "string", 16 | "required": true 17 | }, 18 | "cost": { 19 | "type": "float", 20 | "required": true 21 | } 22 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/invoices.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": { 3 | "type": "string" 4 | }, 5 | "fixed_cost": { 6 | "type": "string", 7 | "transform": "dash" 8 | }, 9 | "number": { 10 | "type": "string", 11 | "required": true 12 | }, 13 | "po_number": { 14 | "type": "string", 15 | "transform": "dash" 16 | }, 17 | "display_date": { 18 | "type": "string", 19 | "transform": "dash", 20 | "required": true 21 | }, 22 | "currency_code": { 23 | "type": "string", 24 | "transform": "dash" 25 | } 26 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/links.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": { 3 | "type": "string", 4 | "required": true 5 | }, 6 | "notify": { 7 | "type": "string" 8 | }, 9 | "notify_current_user": { 10 | "type": "boolean", 11 | "transform": "dash" 12 | }, 13 | "description": { 14 | "type": "string" 15 | }, 16 | "category_id": { 17 | "type": "integer", 18 | "transform": "dash" 19 | }, 20 | "width": { 21 | "type": "string" 22 | }, 23 | "height": { 24 | "type": "string" 25 | }, 26 | "name": { 27 | "type": "string" 28 | }, 29 | "tag_ids": { 30 | "type": "string", 31 | "transform": "camel" 32 | }, 33 | "grant_access_to": { 34 | "type": "string", 35 | "transform": "dash" 36 | }, 37 | "private": { 38 | "type": "boolean" 39 | } 40 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/me/status.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": { 3 | "type": "string" 4 | }, 5 | "notify": { 6 | "type": "string" 7 | } 8 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": { 3 | "type": "string", 4 | "required": true 5 | }, 6 | "body": { 7 | "type": "string", 8 | "required": true 9 | }, 10 | "category_id": { 11 | "type": "string", 12 | "transform": "dash" 13 | }, 14 | "notify": { 15 | "type": "string|array", 16 | "validate": [ 17 | "ALL" 18 | ] 19 | }, 20 | "private": { 21 | "type": "string" 22 | }, 23 | "attachments": { 24 | "type": "string" 25 | }, 26 | "pending_file_attachments": { 27 | "type": "string", 28 | "transform": "camel" 29 | }, 30 | "tags": { 31 | "type": "string" 32 | } 33 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/messages/replies.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": { 3 | "type": "string" 4 | }, 5 | "notify": { 6 | "type": "string|array", 7 | "validate": [ 8 | "ALL" 9 | ] 10 | } 11 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/milestones.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": { 3 | "type": "string", 4 | "required": true 5 | }, 6 | "deadline": { 7 | "type": "string", 8 | "required": true 9 | }, 10 | "responsible_party_ids": { 11 | "type": "string", 12 | "transform": "dash", 13 | "required": true 14 | }, 15 | "change_follower_ids": { 16 | "type": "string", 17 | "transform": "camel" 18 | }, 19 | "description": { 20 | "type": "string" 21 | }, 22 | "notify": { 23 | "type": "boolean" 24 | }, 25 | "tag_ids": { 26 | "type": "string", 27 | "transform": "camel" 28 | }, 29 | "reminder": { 30 | "type": "boolean" 31 | }, 32 | "task_list_ids": { 33 | "type": "array", 34 | "transform": "camel" 35 | }, 36 | "grant_access_to": { 37 | "type": "string", 38 | "transform": "dash" 39 | }, 40 | "private": { 41 | "type": "boolean" 42 | } 43 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/notebooks.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "type": "string", 4 | "required": true 5 | }, 6 | "content": { 7 | "type": "string", 8 | "required": true 9 | }, 10 | "description": { 11 | "type": "string" 12 | }, 13 | "category_id": { 14 | "type": "integer", 15 | "transform": "dash" 16 | }, 17 | "locked": { 18 | "type": "boolean" 19 | }, 20 | "notebook_type": { 21 | "type": "string", 22 | "transform": "dash" 23 | }, 24 | "notify": { 25 | "type": "string" 26 | }, 27 | "notify_current_user": { 28 | "type": "boolean", 29 | "transform": "dash" 30 | }, 31 | "secure_content": { 32 | "type": "boolean", 33 | "transform": "camel" 34 | }, 35 | "grant_access_to": { 36 | "type": "string", 37 | "transform": "dash" 38 | }, 39 | "private": { 40 | "type": "integer" 41 | } 42 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/people.json: -------------------------------------------------------------------------------- 1 | { 2 | "email_address": { 3 | "type": "email", 4 | "transform": "dash", 5 | "required": true 6 | }, 7 | "first_name": { 8 | "type": "string", 9 | "transform": "dash", 10 | "required": true 11 | }, 12 | "last_name": { 13 | "type": "string", 14 | "transform": "dash", 15 | "required": true 16 | }, 17 | "company_id": { 18 | "type": "integer", 19 | "transform": "dash" 20 | }, 21 | "send_invite": { 22 | "type": "boolean", 23 | "transform": "camel" 24 | }, 25 | "title": { 26 | "type": "string" 27 | }, 28 | "phone_number_office": { 29 | "type": "string", 30 | "transform": "dash" 31 | }, 32 | "phone_number_office_ext": { 33 | "type": "string", 34 | "transform": "dash" 35 | }, 36 | "phone_number_mobile_country_code": { 37 | "type": "country", 38 | "transform": "phone-number-mobile-countrycode" 39 | }, 40 | "phone_number_mobile_prefix": { 41 | "type": "string", 42 | "transform": "dash" 43 | }, 44 | "phone_number_mobile": { 45 | "type": "string", 46 | "transform": "dash" 47 | }, 48 | "phone_number_home": { 49 | "type": "string", 50 | "transform": "dash" 51 | }, 52 | "phone_number_fax": { 53 | "type": "string", 54 | "transform": "dash" 55 | }, 56 | "email_alt_1": { 57 | "type": "email", 58 | "transform": "dash" 59 | }, 60 | "email_alt_2": { 61 | "type": "email", 62 | "transform": "dash" 63 | }, 64 | "email_alt_3": { 65 | "type": "email", 66 | "transform": "dash" 67 | }, 68 | "address": { 69 | "type": "object", 70 | "line1": { 71 | "type": "string" 72 | }, 73 | "line2": { 74 | "type": "string" 75 | }, 76 | "city": { 77 | "type": "string" 78 | }, 79 | "state": { 80 | "type": "string" 81 | }, 82 | "zip_code": { 83 | "type": "string", 84 | "transform": "zipcode" 85 | }, 86 | "country_code": { 87 | "type": "country", 88 | "transform": "countrycode" 89 | } 90 | }, 91 | "profile": { 92 | "type": "string" 93 | }, 94 | "user_twitter_name": { 95 | "type": "string", 96 | "transform": "camel" 97 | }, 98 | "user_linkedin": { 99 | "type": "string", 100 | "transform": "camel" 101 | }, 102 | "user_facebook": { 103 | "type": "string", 104 | "transform": "camel" 105 | }, 106 | "user_website": { 107 | "type": "string", 108 | "transform": "camel" 109 | }, 110 | "im_service": { 111 | "type": "string", 112 | "transform": "dash" 113 | }, 114 | "im_handle": { 115 | "type": "string", 116 | "transform": "dash" 117 | }, 118 | "language": { 119 | "type": "string" 120 | }, 121 | "date_format_id": { 122 | "type": "integer", 123 | "transform": "camel" 124 | }, 125 | "time_format_id": { 126 | "type": "integer", 127 | "transform": "camel" 128 | }, 129 | "timezone_id": { 130 | "type": "integer", 131 | "transform": "camel" 132 | }, 133 | "calendar_starts_on_sunday": { 134 | "type": "string", 135 | "transform": "camel" 136 | }, 137 | "length_of_day": { 138 | "type": "integer", 139 | "transform": "camel" 140 | }, 141 | "working_hours": { 142 | "type": "object", 143 | "transform": "camel", 144 | "entries": { 145 | "type": "array", 146 | "properties": { 147 | "weekday": { 148 | "type": "string", 149 | "required": true 150 | }, 151 | "task_hours": { 152 | "type": "integer", 153 | "transform": "camel", 154 | "required": true 155 | } 156 | } 157 | }, 158 | "accept": "" 159 | }, 160 | "change_for_everyone": { 161 | "type": "boolean", 162 | "transform": "camel" 163 | }, 164 | "administrator": { 165 | "type": "boolean" 166 | }, 167 | "can_add_projects": { 168 | "type": "boolean", 169 | "transform": "camel" 170 | }, 171 | "can_manage_people": { 172 | "type": "boolean", 173 | "transform": "camel" 174 | }, 175 | "auto_give_project_access": { 176 | "type": "boolean", 177 | "transform": "camel" 178 | }, 179 | "can_access_calendar": { 180 | "type": "boolean", 181 | "transform": "camel" 182 | }, 183 | "can_access_templates": { 184 | "type": "boolean", 185 | "transform": "camel" 186 | }, 187 | "can_access_portfolio": { 188 | "type": "boolean", 189 | "transform": "camel" 190 | }, 191 | "can_manage_custom_fields": { 192 | "type": "boolean", 193 | "transform": "camel" 194 | }, 195 | "can_manage_portfolio": { 196 | "type": "boolean", 197 | "transform": "camel" 198 | }, 199 | "can_manage_project_templates": { 200 | "type": "boolean", 201 | "transform": "camel" 202 | }, 203 | "can_view_project_templates": { 204 | "type": "boolean", 205 | "transform": "camel" 206 | }, 207 | "notify_on_task_complete": { 208 | "type": "boolean", 209 | "transform": "camel" 210 | }, 211 | "notify_on_added_as_follower": { 212 | "type": "boolean", 213 | "transform": "dash" 214 | }, 215 | "notify_on_status_update": { 216 | "type": "boolean", 217 | "transform": "dash" 218 | }, 219 | "text_format": { 220 | "type": "string", 221 | "transform": "camel" 222 | }, 223 | "use_shorthand_durations": { 224 | "type": "boolean", 225 | "transform": "camel" 226 | }, 227 | "user_receive_notify_warnings": { 228 | "type": "boolean", 229 | "transform": "camel" 230 | }, 231 | "user_receive_my_notifications_only": { 232 | "type": "boolean", 233 | "transform": "camel" 234 | }, 235 | "receive_daily_reports": { 236 | "type": "boolean", 237 | "transform": "camel" 238 | }, 239 | "receive_daily_reports_at_weekend": { 240 | "type": "boolean", 241 | "transform": "camel" 242 | }, 243 | "receive_daily_reports_if_empty": { 244 | "type": "boolean", 245 | "transform": "camel" 246 | }, 247 | "sound_alerts_enabled": { 248 | "type": "boolean", 249 | "transform": "camel" 250 | }, 251 | "daily_report_sort": { 252 | "type": "string", 253 | "transform": "camel" 254 | }, 255 | "receive_daily_reports_at_time": { 256 | "type": "string", 257 | "transform": "camel" 258 | }, 259 | "daily_report_events_type": { 260 | "type": "string", 261 | "transform": "camel" 262 | }, 263 | "daily_report_days_filter": { 264 | "type": "integer", 265 | "transform": "camel" 266 | }, 267 | "avatar_pending_file_ref": { 268 | "type": "string", 269 | "transform": "camel" 270 | }, 271 | "remove_avatar": { 272 | "type": "boolean", 273 | "transform": "camel" 274 | }, 275 | "allow_email_notifications": { 276 | "type": "email", 277 | "transform": "camel" 278 | }, 279 | "user_type": { 280 | "type": "string", 281 | "transform": "dash" 282 | }, 283 | "private_notes": { 284 | "type": "string", 285 | "transform": "camel" 286 | }, 287 | "get_user_details": { 288 | "type": "boolean", 289 | "transform": "camel" 290 | } 291 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/people/status.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": { 3 | "type": "string" 4 | }, 5 | "notify": { 6 | "type": "boolean" 7 | }, 8 | "notify_ids": { 9 | "type": "array", 10 | "transform": "camel" 11 | } 12 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/portfolio/boards.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "type": "string" 4 | }, 5 | "description": { 6 | "type": "string" 7 | }, 8 | "color": { 9 | "type": "string" 10 | } 11 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/portfolio/cards.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectId": { 3 | "type": "integer", 4 | "required": true 5 | } 6 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/portfolio/columns.json: -------------------------------------------------------------------------------- 1 | { 2 | "color": { 3 | "type": "string" 4 | }, 5 | "name": { 6 | "type": "string" 7 | } 8 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/projects.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "type": "string", 4 | "required": true 5 | }, 6 | "description": { 7 | "type": "string" 8 | }, 9 | "use_tasks": { 10 | "type": "boolean", 11 | "transform": "dash" 12 | }, 13 | "use_milestones": { 14 | "type": "boolean", 15 | "transform": "dash" 16 | }, 17 | "use_messages": { 18 | "type": "boolean", 19 | "transform": "dash" 20 | }, 21 | "use_files": { 22 | "type": "boolean", 23 | "transform": "dash" 24 | }, 25 | "use_time": { 26 | "type": "boolean", 27 | "transform": "dash" 28 | }, 29 | "use_notebook": { 30 | "type": "boolean", 31 | "transform": "dash" 32 | }, 33 | "use_risk_register": { 34 | "type": "boolean", 35 | "transform": "use-riskregister" 36 | }, 37 | "use_links": { 38 | "type": "boolean", 39 | "transform": "dash" 40 | }, 41 | "use_billing": { 42 | "type": "boolean", 43 | "transform": "dash" 44 | }, 45 | "use_comments": { 46 | "type": "boolean", 47 | "transform": "dash" 48 | }, 49 | "category_id": { 50 | "type": "integer", 51 | "transform": "dash" 52 | }, 53 | "start_date": { 54 | "type": "string", 55 | "transform": "dash" 56 | }, 57 | "end_date": { 58 | "type": "string", 59 | "transform": "dash" 60 | }, 61 | "tag_ids": { 62 | "type": "string", 63 | "transform": "camel" 64 | }, 65 | "onboarding": { 66 | "type": "boolean" 67 | }, 68 | "grant_access_to": { 69 | "type": "string", 70 | "transform": "dash" 71 | }, 72 | "private": { 73 | "type": "boolean" 74 | }, 75 | "custom_fields": { 76 | "type": "array", 77 | "transform": "camel", 78 | "properties": { 79 | "custom_field_id": { 80 | "type": "integer", 81 | "transform": "camel", 82 | "required": true 83 | }, 84 | "value": { 85 | "type": "string", 86 | "required": true 87 | } 88 | }, 89 | "accept": "" 90 | }, 91 | "people": { 92 | "type": "integer" 93 | }, 94 | "project_owner_id": { 95 | "type": "integer", 96 | "transform": "camel" 97 | }, 98 | "company_id": { 99 | "type": "integer", 100 | "transform": "camel" 101 | }, 102 | "template_date_target_default": { 103 | "type": "string", 104 | "transform": "camel", 105 | "on_update": true 106 | } 107 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/projects/custom_fields.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "type": "string", 4 | "required": true 5 | }, 6 | "entity": { 7 | "type": "string", 8 | "required": true, 9 | "validate": [ 10 | "project", 11 | "task" 12 | ] 13 | }, 14 | "type": { 15 | "type": "string", 16 | "required": true, 17 | "validate": [ 18 | "text-short", 19 | "number-integer", 20 | "date", 21 | "url", 22 | "checkbox", 23 | "dropdown", 24 | "status" 25 | ] 26 | }, 27 | "description": { 28 | "type": "string" 29 | }, 30 | "formula": { 31 | "type": "string" 32 | }, 33 | "project_id": { 34 | "type": "integer", 35 | "transform": "camel" 36 | }, 37 | "required": { 38 | "type": "boolean" 39 | }, 40 | "is_private": { 41 | "type": "boolean", 42 | "transform": "camel" 43 | }, 44 | "options": { 45 | "type": "object", 46 | "accept": "", 47 | "choices": { 48 | "type": "array", 49 | "properties": { 50 | "color": { 51 | "type": "string", 52 | "required": true 53 | }, 54 | "value": { 55 | "type": "string", 56 | "required": true 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/projects/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "type": "string" 4 | }, 5 | "description": { 6 | "type": "string" 7 | }, 8 | "private": { 9 | "type": "integer" 10 | }, 11 | "category_id": { 12 | "type": "integer", 13 | "transform": "dash" 14 | }, 15 | "category_name": { 16 | "type": "string", 17 | "transform": "dash" 18 | }, 19 | "pending_file_ref": { 20 | "type": "string|array", 21 | "transform": "camel", 22 | "required": true 23 | }, 24 | "tags": { 25 | "type": "array" 26 | }, 27 | "notify": { 28 | "type": "string|array", 29 | "validate": [ 30 | "ALL" 31 | ] 32 | }, 33 | "notify_current_user": { 34 | "type": "boolean", 35 | "transform": "camel" 36 | }, 37 | "auto_new_version": { 38 | "type": "boolean", 39 | "transform": "camel" 40 | }, 41 | "grant_access_to": { 42 | "type": "string|array", 43 | "validate": [ 44 | "" 45 | ], 46 | "transform": "dash" 47 | }, 48 | "tag_ids": { 49 | "type": "array", 50 | "transform": "camel" 51 | } 52 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/projects/rates.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_default": { 3 | "type": "integer", 4 | "transform": "dash" 5 | }, 6 | "users": { 7 | "type": "array", 8 | "properties": { 9 | "$key": { 10 | "type": "integer" 11 | }, 12 | "rate": { 13 | "type": "integer" 14 | } 15 | }, 16 | "accept": "<$key,rate>" 17 | } 18 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/projects/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "type": "string" 4 | }, 5 | "description": { 6 | "type": "string" 7 | }, 8 | "use_tasks": { 9 | "type": "boolean", 10 | "transform": "dash" 11 | }, 12 | "use_milestones": { 13 | "type": "boolean", 14 | "transform": "dash" 15 | }, 16 | "use_messages": { 17 | "type": "boolean", 18 | "transform": "dash" 19 | }, 20 | "use_files": { 21 | "type": "boolean", 22 | "transform": "dash" 23 | }, 24 | "use_time": { 25 | "type": "boolean", 26 | "transform": "dash" 27 | }, 28 | "use_notebook": { 29 | "type": "boolean", 30 | "transform": "dash" 31 | }, 32 | "use_riskregister": { 33 | "type": "boolean", 34 | "transform": "dash" 35 | }, 36 | "use_links": { 37 | "type": "boolean", 38 | "transform": "dash" 39 | }, 40 | "use_billing": { 41 | "type": "boolean", 42 | "transform": "dash" 43 | }, 44 | "use_comments": { 45 | "type": "boolean", 46 | "transform": "dash" 47 | }, 48 | "category_id": { 49 | "type": "integer", 50 | "transform": "dash" 51 | }, 52 | "tags": { 53 | "type": "string" 54 | }, 55 | "template_date_target_default": { 56 | "type": "string", 57 | "transform": "camel" 58 | }, 59 | "people": { 60 | "type": "string" 61 | }, 62 | "project_owner_id": { 63 | "type": "string", 64 | "transform": "camel" 65 | }, 66 | "company_id": { 67 | "type": "integer", 68 | "transform": "camel" 69 | }, 70 | "grant_access_to": { 71 | "type": "string", 72 | "transform": "dash" 73 | } 74 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/resource_categories.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "type": "string", 4 | "required": true 5 | }, 6 | "parent_id": { 7 | "type": "string", 8 | "transform": "dash" 9 | } 10 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/resource_comments.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": { 3 | "type": "string", 4 | "required": true 5 | }, 6 | "notify": { 7 | "type": "string" 8 | }, 9 | "is_private": { 10 | "type": "boolean", 11 | "transform": "isprivate" 12 | }, 13 | "pending_file_attachments": { 14 | "type": "string", 15 | "transform": "camel" 16 | }, 17 | "content_type": { 18 | "type": "string", 19 | "transform": "dash" 20 | } 21 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/roles.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": { 3 | "type": "string|array", 4 | "required": true 5 | }, 6 | "name": { 7 | "type": "string", 8 | "required": true 9 | }, 10 | "description": { 11 | "type": "string" 12 | } 13 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/tags.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "required": true, 4 | "type": "string" 5 | }, 6 | "color": { 7 | "type": "string" 8 | } 9 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/tasklists.json: -------------------------------------------------------------------------------- 1 | { 2 | "apply_defaults_to_existing_tasks": { 3 | "type": "boolean", 4 | "transform": "camel", 5 | "sibling": true 6 | }, 7 | "todo_list": { 8 | "type": "object", 9 | "transform": "dash", 10 | "sibling": true, 11 | "name": { 12 | "type": "string", 13 | "required": true 14 | }, 15 | "description": { 16 | "type": "string" 17 | }, 18 | "private": { 19 | "type": "boolean" 20 | }, 21 | "pinned": { 22 | "type": "boolean" 23 | }, 24 | "is_billable": { 25 | "type": "integer", 26 | "transform": "camel" 27 | }, 28 | "milestone_id": { 29 | "type": "integer", 30 | "transform": "milestone-Id" 31 | }, 32 | "grant_access_to": { 33 | "type": "string", 34 | "transform": "dash" 35 | }, 36 | "todo_list_template_id": { 37 | "type": "integer", 38 | "transform": "camel" 39 | }, 40 | "new_task_defaults": { 41 | "type": "object", 42 | "transform": "dash", 43 | "description": { 44 | "type": "string" 45 | }, 46 | "start_date_offset": { 47 | "type": "string", 48 | "transform": "dash" 49 | }, 50 | "due_date_offset": { 51 | "type": "string", 52 | "transform": "dash" 53 | }, 54 | "responsible_party_id": { 55 | "type": "string", 56 | "transform": "dash" 57 | }, 58 | "priority": { 59 | "type": "integer" 60 | }, 61 | "priority_text": { 62 | "type": "string", 63 | "transform": "camel" 64 | }, 65 | "estimated_minutes": { 66 | "type": "integer", 67 | "transform": "dash" 68 | }, 69 | "tags": { 70 | "type": "array", 71 | "properties": { 72 | "id": { 73 | "type": "integer" 74 | }, 75 | "color": { 76 | "type": "string" 77 | }, 78 | "name": { 79 | "type": "string" 80 | }, 81 | "project_id": { 82 | "type": "integer", 83 | "transform": "camel" 84 | } 85 | } 86 | }, 87 | "column_id": { 88 | "type": "integer", 89 | "transform": "dash" 90 | }, 91 | "reminders": { 92 | "type": "array", 93 | "properties": { 94 | "user_id": { 95 | "type": "integer", 96 | "transform": "dash" 97 | }, 98 | "type": { 99 | "type": "string" 100 | }, 101 | "note": { 102 | "type": "string" 103 | }, 104 | "people_assigned": { 105 | "type": "boolean", 106 | "transform": "dash" 107 | }, 108 | "is_relative": { 109 | "type": "boolean", 110 | "transform": "camel" 111 | }, 112 | "relative_number_days": { 113 | "type": "integer", 114 | "transform": "dash" 115 | }, 116 | "using_off_set_due_date": { 117 | "type": "boolean", 118 | "transform": "camel" 119 | }, 120 | "time": { 121 | "type": "string" 122 | } 123 | } 124 | }, 125 | "remove_all_reminders": { 126 | "type": "boolean", 127 | "transform": "camel" 128 | }, 129 | "comment_follower_ids": { 130 | "type": "string", 131 | "transform": "dash" 132 | }, 133 | "change_follower_ids": { 134 | "type": "string", 135 | "transform": "dash" 136 | }, 137 | "grant_access_to": { 138 | "type": "string", 139 | "transform": "dash" 140 | }, 141 | "private": { 142 | "type": "boolean" 143 | }, 144 | "custom_fields": { 145 | "type": "array", 146 | "transform": "camel", 147 | "properties": { 148 | "custom_field_id": { 149 | "type": "integer", 150 | "transform": "camel" 151 | }, 152 | "value": { 153 | "type": "string" 154 | } 155 | } 156 | }, 157 | "pending_file_attachments": { 158 | "type": "array", 159 | "transform": "camel", 160 | "properties": { 161 | "type": "object" 162 | } 163 | } 164 | } 165 | } 166 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "use_defaults": { 3 | "type": "boolean", 4 | "transform": "dash" 5 | }, 6 | "completed": { 7 | "type": "boolean" 8 | }, 9 | "content": { 10 | "type": "string", 11 | "required": true 12 | }, 13 | "task_list_id": { 14 | "type": "integer", 15 | "transform": "tasklistId" 16 | }, 17 | "creator_id": { 18 | "type": "integer", 19 | "transform": "dash" 20 | }, 21 | "notify": { 22 | "type": "boolean" 23 | }, 24 | "responsible_party_id": { 25 | "type": "string", 26 | "transform": "dash" 27 | }, 28 | "start_date": { 29 | "type": "string", 30 | "transform": "dash" 31 | }, 32 | "due_date": { 33 | "type": "string", 34 | "transform": "dash" 35 | }, 36 | "description": { 37 | "type": "string" 38 | }, 39 | "priority": { 40 | "type": "string" 41 | }, 42 | "progress": { 43 | "type": "integer" 44 | }, 45 | "parent_task_id": { 46 | "type": "integer", 47 | "transform": "camel" 48 | }, 49 | "tag_ids": { 50 | "type": "string", 51 | "transform": "camel" 52 | }, 53 | "everyone_must_do": { 54 | "type": "boolean", 55 | "transform": "dash" 56 | }, 57 | "predecessors": { 58 | "type": "array", 59 | "properties": { 60 | "id": { 61 | "type": "integer" 62 | }, 63 | "name": { 64 | "type": "string" 65 | }, 66 | "type": { 67 | "type": "string" 68 | }, 69 | "status": { 70 | "type": "string" 71 | }, 72 | "parent_task_id": { 73 | "type": "integer", 74 | "transform": "camel" 75 | } 76 | } 77 | }, 78 | "reminders": { 79 | "type": "array", 80 | "properties": { 81 | "user_id": { 82 | "type": "integer", 83 | "transform": "dash" 84 | }, 85 | "date_time_utc": { 86 | "type": "string", 87 | "transform": "dash" 88 | }, 89 | "type": { 90 | "type": "string" 91 | }, 92 | "note": { 93 | "type": "string" 94 | }, 95 | "people_assigned": { 96 | "type": "boolean", 97 | "transform": "dash" 98 | }, 99 | "is_relative": { 100 | "type": "boolean", 101 | "transform": "camel" 102 | }, 103 | "relative_number_days": { 104 | "type": "integer", 105 | "transform": "dash" 106 | }, 107 | "using_off_set_due_date": { 108 | "type": "boolean", 109 | "transform": "camel" 110 | } 111 | } 112 | }, 113 | "column_id": { 114 | "type": "integer", 115 | "transform": "camel" 116 | }, 117 | "comment_follower_ids": { 118 | "type": "string", 119 | "transform": "camel" 120 | }, 121 | "change_follower_ids": { 122 | "type": "string", 123 | "transform": "camel" 124 | }, 125 | "grant_access_to": { 126 | "type": "string", 127 | "transform": "dash" 128 | }, 129 | "private": { 130 | "type": "boolean" 131 | }, 132 | "custom_fields": { 133 | "type": "array", 134 | "transform": "camel", 135 | "properties": { 136 | "custom_field_id": { 137 | "type": "integer", 138 | "transform": "camel", 139 | "required": true 140 | }, 141 | "value": { 142 | "type": "string", 143 | "required": true 144 | } 145 | }, 146 | "accept": "" 147 | }, 148 | "estimated_minutes": { 149 | "type": "integer", 150 | "transform": "dash" 151 | }, 152 | "pending_file_attachments": { 153 | "type": "string|array", 154 | "transform": "camel" 155 | }, 156 | "update_files": { 157 | "type": "boolean", 158 | "transform": "camel" 159 | }, 160 | "attachments": { 161 | "type": "string" 162 | }, 163 | "remove_other_files": { 164 | "type": "boolean", 165 | "transform": "camel" 166 | }, 167 | "attachments_category_ids": { 168 | "type": "string", 169 | "transform": "camel" 170 | }, 171 | "pending_file_attachments_category_ids": { 172 | "type": "string", 173 | "transform": "camel" 174 | }, 175 | "repeat_options": { 176 | "type": "object", 177 | "transform": "camel", 178 | "selected_days": { 179 | "type": "string", 180 | "transform": "selecteddays" 181 | }, 182 | "repeat_end_date": { 183 | "type": "string", 184 | "transform": "camel" 185 | }, 186 | "repeats_freq": { 187 | "type": "string", 188 | "transform": "camel" 189 | }, 190 | "monthly_repeat_type": { 191 | "type": "string", 192 | "transform": "camel" 193 | } 194 | }, 195 | "tags": { 196 | "type": "string", 197 | "on_update": true 198 | }, 199 | "position_after_task": { 200 | "type": "integer", 201 | "transform": "camel", 202 | "on_update": true 203 | } 204 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/tasks/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "pending_file_attachments": { 3 | "type": "array", 4 | "transform": "camel" 5 | }, 6 | "update_files": { 7 | "type": "boolean", 8 | "transform": "camel" 9 | }, 10 | "remove_other_files": { 11 | "type": "boolean", 12 | "transform": "camel" 13 | }, 14 | "attachments": { 15 | "type": "string" 16 | }, 17 | "attachments_category_ids": { 18 | "type": "string", 19 | "transform": "camel" 20 | }, 21 | "pending_file_attachments_category_ids": { 22 | "type": "string", 23 | "transform": "camel" 24 | } 25 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/teams.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent_team_id": { 3 | "type": "integer", 4 | "transform": "camel" 5 | }, 6 | "name": { 7 | "type": "string" 8 | }, 9 | "description": { 10 | "type": "string" 11 | }, 12 | "logo_pending_file_ref": { 13 | "type": "string", 14 | "transform": "camel" 15 | }, 16 | "logo_icon": { 17 | "type": "string", 18 | "transform": "camel" 19 | }, 20 | "logo_color": { 21 | "type": "string", 22 | "transform": "camel" 23 | }, 24 | "handle": { 25 | "type": "string" 26 | }, 27 | "company_id": { 28 | "type": "integer", 29 | "transform": "camel" 30 | }, 31 | "project_id": { 32 | "type": "integer", 33 | "transform": "camel" 34 | }, 35 | "user_ids": { 36 | "type": "string", 37 | "transform": "camel" 38 | }, 39 | "default_project_ids": { 40 | "type": "string", 41 | "transform": "camel" 42 | }, 43 | "is_private": { 44 | "type": "boolean", 45 | "transform": "camel" 46 | }, 47 | "get": { 48 | "type": "string" 49 | } 50 | } -------------------------------------------------------------------------------- /src/Rest/Resource/schemas/time_entries.json: -------------------------------------------------------------------------------- 1 | { 2 | "date": { 3 | "type": "string", 4 | "required": true, 5 | "length": 6 6 | }, 7 | "hours": { 8 | "type": "integer", 9 | "length": 3 10 | }, 11 | "minutes": { 12 | "type": "integer", 13 | "length": 2 14 | }, 15 | "description": { 16 | "type": "string" 17 | }, 18 | "person_id": { 19 | "type": "integer", 20 | "transform": "dash" 21 | }, 22 | "time": { 23 | "type": "string" 24 | }, 25 | "is_billable": { 26 | "type": "boolean", 27 | "transform": "isbillable" 28 | }, 29 | "tags": { 30 | "type": "string" 31 | } 32 | } -------------------------------------------------------------------------------- /src/Rest/Response/JSON.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class JSON extends Model 16 | { 17 | public function parse(string $data, array $headers): static | int | bool | string | null 18 | { 19 | /** 20 | * @var mixed 21 | */ 22 | $source = json_decode($data, true); 23 | $errors = $this->getJsonErrors(); 24 | $this->originalString = $this->string = $data; 25 | 26 | [ 27 | 'Status' => $status, 28 | 'X-Action' => $action, 29 | 'X-Parent' => $wrapper, 30 | 'Method' => $method 31 | ] = $headers; 32 | 33 | if (!in_array($status, [200, 201, 204])) { 34 | if ($status === 500 && isset($source['content'])) { 35 | $source = $source['content']; 36 | if (isset($source['errors']) && is_array($source['errors'])) { 37 | $source['error'] = implode(PHP_EOL, $source['errors']); 38 | } 39 | } 40 | $errors = $source['MESSAGE'] ?? $source['error'] ?? $errors ?? "Unknown error ($status) status"; 41 | } 42 | if ($errors !== null) { 43 | throw new Exception([ 44 | 'Message' => $errors, 45 | 'Response' => $data, 46 | 'Headers' => $headers, 47 | ]); 48 | } 49 | 50 | switch ($method) { 51 | case 'POST': 52 | if (!empty($headers['id']) && $status === 201) { 53 | return (int) $headers['id']; 54 | } 55 | 56 | if (!empty($source['fileId'])) { 57 | return (int) $source['fileId']; 58 | } 59 | 60 | if (!empty($source['pendingFile']['ref'])) { 61 | return $source['pendingFile']['ref']; 62 | } 63 | 64 | if (isset($source[$wrapper])) { 65 | /** 66 | * @var mixed 67 | */ 68 | $source = $source[$wrapper]; 69 | } 70 | // no break 71 | case 'PUT': 72 | unset($source['STATUS']); 73 | $keys = array_keys($source); 74 | if ($headers['X-Not-Use-Files'] && 75 | ($count = count($keys)) && ( 76 | $count != 1 || !in_array('id', $keys) 77 | ) 78 | ) { 79 | if ( 80 | preg_match('!/(\d+)/tags!', $action) && !empty($source['tags']) 81 | ) { 82 | return true; 83 | } 84 | if ( 85 | preg_match('!/(\d+)/restore!', $action) && !empty($source['restoredItems']) 86 | ) { 87 | return true; 88 | } 89 | if ( 90 | preg_match('!/(\d+)/(react|unreact)!', $action) && !empty($source['reactions']) 91 | ) { 92 | $source = $source['reactions']; 93 | } 94 | /** 95 | * @var \stdClass 96 | */ 97 | $data = static::camelizeObject($source); 98 | if (!empty($data->id)) { 99 | $data->id = (int) $data->id; 100 | } 101 | /** @psalm-suppress InvalidPropertyAssignmentValue */ 102 | $this->data = $data; 103 | return $this; 104 | } 105 | $id = $source['id'] ?? null; 106 | if ($id !== null) { 107 | $id = (int) $id; 108 | } 109 | return $id ?? true; 110 | case 'DELETE': 111 | return true; 112 | 113 | default: 114 | /** 115 | * @var array 116 | */ 117 | if (!empty($source['STATUS'])) { 118 | unset($source['STATUS']); 119 | } 120 | 121 | $count = count($source); 122 | if ($count == 1) { 123 | $key = key($source); 124 | $match = $wrapper == $key; 125 | $source = $match ? $source[$wrapper] : current($source); 126 | if ($source) { 127 | if (preg_match('!projects/(\d+)/time/total!', $action)) { 128 | $source = current($source); 129 | } elseif ( 130 | preg_match('!messageReplies/(\d+)!', $action) 131 | ) { 132 | $source = current($source); 133 | } elseif (preg_match('!tasklists/(\d+)/time/total!', $action)) { 134 | $source = current($source); 135 | $source = $source['tasklist']; 136 | } elseif (preg_match('!tasks/(\d+)/time/total!', $action)) { 137 | $source = current($source); 138 | $source = $source['tasklist']['task']; 139 | } 140 | } 141 | 142 | if ($key === 'project') { 143 | foreach(['files'] as $key) { 144 | if (isset($source[$key])) { 145 | $source = $source[$key]; 146 | break; 147 | } 148 | } 149 | } elseif ($key === 'projects' && $action === 'files') { 150 | $data = []; 151 | foreach($source as $project) { 152 | foreach ($project['files'] as $file) { 153 | $data[] = $file; 154 | } 155 | } 156 | $source = $data; 157 | } 158 | } elseif ( 159 | isset($source['card']) 160 | && preg_match('!portfolio/cards/(\d+)!', $action) 161 | ) { 162 | $source = $source['card']; 163 | } 164 | 165 | $this->headers = $headers; 166 | $this->string = json_encode($source); 167 | if (is_arr_obj($source)) { 168 | /** 169 | * @var \stdClass 170 | */ 171 | $data = static::camelizeObject($source); 172 | if (!empty($data->id)) { 173 | $data->id = (int)$data->id; 174 | } 175 | /** @psalm-suppress InvalidPropertyAssignmentValue */ 176 | $this->data = $data; 177 | } 178 | 179 | return $this; 180 | } 181 | } 182 | 183 | /** 184 | * @return string 185 | */ 186 | protected function getContent(): string 187 | { 188 | /** 189 | * @var object 190 | */ 191 | $object = json_decode((string) $this->string); 192 | 193 | return json_encode($object, JSON_PRETTY_PRINT); 194 | } 195 | 196 | /** 197 | * @return string 198 | */ 199 | public function getOriginalContent(): string 200 | { 201 | /** 202 | * @var object 203 | */ 204 | $object = json_decode((string) $this->originalString); 205 | 206 | return json_encode($object, JSON_PRETTY_PRINT); 207 | } 208 | 209 | /** 210 | * @param mixed $source 211 | * 212 | * @return \ArrayObject|mixed 213 | */ 214 | protected static function camelizeObject(mixed $source) 215 | { 216 | if (!is_arr_obj($source)) { 217 | return $source; 218 | } 219 | 220 | $destination = new \ArrayObject([], \ArrayObject::ARRAY_AS_PROPS); 221 | /** 222 | * @var string $key 223 | * @var mixed $value 224 | */ 225 | foreach ($source as $key => $value) { 226 | if (ctype_upper((string) $key)) { 227 | $key = strtolower($key); 228 | } 229 | $key = Str::camel($key); 230 | $destination->$key = is_scalar($value) ? $value : static::camelizeObject($value); 231 | } 232 | 233 | return $destination; 234 | } 235 | 236 | private function getJsonErrors(): ?string 237 | { 238 | $errorCode = json_last_error(); 239 | if (!$errorCode) { 240 | return null; 241 | } 242 | 243 | return json_last_error_msg(); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/Rest/Response/Model.php: -------------------------------------------------------------------------------- 1 | 11 | * @implements \IteratorAggregate 12 | */ 13 | abstract class Model implements \IteratorAggregate, \Countable, \ArrayAccess 14 | { 15 | protected ?string $string = null; 16 | 17 | protected ?string $originalString = null; 18 | 19 | protected array $headers = []; 20 | 21 | /** 22 | * @var array|\ArrayObject 23 | */ 24 | protected array|\ArrayObject $data = []; 25 | 26 | final public function __construct() 27 | { 28 | } 29 | 30 | /** 31 | * 32 | * @param string $data 33 | * @param array $headers 34 | * @return static|int|bool|null 35 | */ 36 | abstract public function parse(string $data, array $headers): static|int|bool|string|null; 37 | 38 | public function save(string $filename): bool 39 | { 40 | if (strpos($filename, '.') === false) { 41 | $class = static::class; 42 | $ext = strtolower(substr($class, (int) strrpos($class, '\\') + 1)); 43 | $filename .= '.' . $ext; 44 | } 45 | $dirname = dirname($filename); 46 | // create the directory if it does not exist 47 | if ($dirname && !is_dir($dirname)) { 48 | mkdir($dirname, 0777, true); 49 | } 50 | 51 | return file_put_contents($filename, $this->getContent()) !== false; 52 | } 53 | 54 | /** 55 | * 56 | * @return string 57 | */ 58 | abstract protected function getContent(): string ; 59 | 60 | /** 61 | * 62 | * @return string 63 | */ 64 | abstract public function getOriginalContent(): string; 65 | 66 | public function __toString(): string 67 | { 68 | return $this->getContent(); 69 | } 70 | 71 | public function toArray(): array 72 | { 73 | return (array) $this->data; 74 | } 75 | 76 | public function getHeaders(): array 77 | { 78 | return $this->headers; 79 | } 80 | 81 | /** 82 | * 83 | * @return \Traversable 84 | */ 85 | public function getIterator(): \Traversable 86 | { 87 | return new \ArrayIterator((array) $this->data); 88 | } 89 | 90 | public function count(): int 91 | { 92 | return count($this->data); 93 | } 94 | 95 | /** 96 | * 97 | * @param TKey|null $offset 98 | * @param mixed $value 99 | * @return void 100 | */ 101 | public function offsetSet(mixed $offset, mixed $value): void 102 | { 103 | if ($offset === null) { 104 | /** @psalm-suppress NullArgument */ 105 | $this->data[] = $value; 106 | } else { 107 | $this->data[$offset] = $value; 108 | } 109 | } 110 | 111 | /** 112 | * 113 | * @param TKey $offset 114 | * @return boolean 115 | */ 116 | public function offsetExists(mixed $offset): bool 117 | { 118 | return isset($this->data[$offset]); 119 | } 120 | 121 | /** 122 | * 123 | * @param TKey $offset 124 | * @return void 125 | */ 126 | public function offsetUnset(mixed $offset): void 127 | { 128 | unset($this->data[$offset]); 129 | } 130 | 131 | /** 132 | * 133 | * @param TKey $offset 134 | * @return mixed 135 | */ 136 | public function offsetGet(mixed $offset): mixed 137 | { 138 | return $this->data[$offset] ?? null; 139 | } 140 | 141 | /** 142 | * 143 | * @param TKey $name 144 | * @return mixed 145 | */ 146 | public function __get(mixed $name) 147 | { 148 | return $this->data[$name] ?? null; 149 | } 150 | 151 | /** 152 | * Undocumented function 153 | * 154 | * @param TKey $name 155 | * @param mixed $value 156 | */ 157 | public function __set(mixed $name, mixed $value): void 158 | { 159 | $this->data[$name] = $value; 160 | } 161 | 162 | /** 163 | * 164 | * @param TKey $name 165 | * @return boolean 166 | */ 167 | public function __isset(mixed $name) 168 | { 169 | return isset($this->data[$name]); 170 | } 171 | 172 | /** 173 | * 174 | * @param TKey $name 175 | */ 176 | public function __unset(mixed $name) 177 | { 178 | unset($this->data[$name]); 179 | } 180 | 181 | public function map(callable $callback) 182 | { 183 | $data = []; 184 | 185 | foreach ($this as $key => $value) { 186 | $data[] = $callback($value, $key); 187 | } 188 | 189 | return $data; 190 | } 191 | 192 | public function reduce(callable $callback, mixed $initial = []): mixed 193 | { 194 | /** 195 | * @var mixed 196 | */ 197 | $accumulator = $initial; 198 | foreach ($this as $key => $value) { 199 | /** 200 | * @var mixed 201 | */ 202 | $accumulator = $callback($accumulator, $value, $key); 203 | } 204 | 205 | return $accumulator; 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/Rest/Response/XML.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class XML extends Model 17 | { 18 | /** 19 | * Parsea un string type xml 20 | * 21 | * @param string $data 22 | * @param type $headers 23 | * @return mixed [bool, int, TeamWorkPm\Response\XML] 24 | * @throws Exception 25 | */ 26 | public function parse($data, array $headers) 27 | { 28 | libxml_use_internal_errors(true); 29 | $this->originalString = $this->string = $data; 30 | $source = simplexml_load_string($data); 31 | $errors = $this->getXmlErrors($source); 32 | if ($source) { 33 | if (in_array($headers['Status'], [201, 200, 204])) { 34 | switch ($headers['Method']) { 35 | case 'UPLOAD': 36 | if (!empty($source->ref)) { 37 | return (string)$source->ref; 38 | } 39 | break; 40 | case 'POST': 41 | if (!empty($headers['id'])) { 42 | return $headers['id']; 43 | } 44 | 45 | $property = 0; 46 | $value = (int)$source->$property; 47 | // this case the fileid 48 | if ($value > 0) { 49 | return $value; 50 | } 51 | break; 52 | case 'PUT': 53 | case 'DELETE': 54 | return true; 55 | default: 56 | if (!empty($source->files->file)) { 57 | $source = $source->files->file; 58 | $isArray = true; 59 | } elseif (!empty($source->notebooks->notebook)) { 60 | $source = $source->notebooks->notebook; 61 | $isArray = true; 62 | } elseif (!empty($source->project->links)) { 63 | $source = $source->project->links; 64 | $isArray = true; 65 | } else { 66 | $attrs = $source->attributes(); 67 | $isArray = !empty($attrs->type) && (string)$attrs->type === 'array'; 68 | } 69 | $this->headers = $headers; 70 | 71 | $_this = self::toStdClass($source, $isArray); 72 | 73 | foreach ($_this as $key => $value) { 74 | $this->$key = $value; 75 | } 76 | return $this; 77 | } 78 | } else { 79 | if (!empty($source->error)) { 80 | foreach ($source as $error) { 81 | $errors .= $error . "\n"; 82 | } 83 | } else { 84 | $property = 0; 85 | $errors .= $source->$property; 86 | } 87 | } 88 | } 89 | throw new Exception([ 90 | 'Message' => $errors, 91 | 'Response' => $data, 92 | 'Headers' => $headers, 93 | ]); 94 | } 95 | 96 | /** 97 | * 98 | * @return string 99 | */ 100 | protected function getContent(): string 101 | { 102 | $dom = new \DOMDocument('1.0'); 103 | $dom->loadXML($this->string); 104 | $dom->preserveWhiteSpace = false; 105 | $dom->formatOutput = true; 106 | 107 | return $dom->saveXML(); 108 | } 109 | 110 | /** 111 | * 112 | * @return string 113 | */ 114 | public function getOriginalContent(): string 115 | { 116 | $dom = new \DOMDocument('1.0'); 117 | $dom->loadXML($this->originalString); 118 | $dom->preserveWhiteSpace = false; 119 | $dom->formatOutput = true; 120 | 121 | return $dom->saveXML(); 122 | } 123 | 124 | /** 125 | * Convierte un objecto SimpleXMLElement a stdClass 126 | * 127 | * @param \SimpleXMLElement $source 128 | * @param bool $isArray 129 | * @return \stdClass 130 | */ 131 | private static function toStdClass( 132 | \SimpleXMLElement $source, 133 | $isArray = false 134 | ) { 135 | $destination = $isArray ? [] : new \stdClass(); 136 | foreach ($source as $key => $value) { 137 | $key = Str::camel($key); 138 | $attrs = $value->attributes(); 139 | if (!empty($attrs->type)) { 140 | $type = (string)$attrs->type; 141 | switch ($type) { 142 | case 'integer': 143 | $destination->$key = (int)$value; 144 | break; 145 | case 'boolean': 146 | $value = (string)$value; 147 | $destination->$key = (bool)$value === 'true'; 148 | break; 149 | case 'array': 150 | if (is_array($destination)) { 151 | $destination[$key] = self::toStdClass($value, true); 152 | } else { 153 | $destination->$key = self::toStdClass($value, true); 154 | } 155 | break; 156 | default: 157 | $destination->$key = (string)$value; 158 | break; 159 | } 160 | } else { 161 | $children = $value->children(); 162 | if (!empty($children)) { 163 | if ($isArray) { 164 | $i = count($destination); 165 | $destination[$i] = self::toStdClass($value); 166 | } else { 167 | $destination->$key = self::toStdClass($value); 168 | } 169 | } else { 170 | $destination->$key = (string)$value; 171 | } 172 | } 173 | } 174 | 175 | return $destination; 176 | } 177 | 178 | private function getXmlErrors($xml) 179 | { 180 | $errors = ''; 181 | foreach (libxml_get_errors() as $error) { 182 | $errors .= $this->getXmlError($error, $xml) . "\n"; 183 | } 184 | libxml_clear_errors(); 185 | return $errors; 186 | } 187 | 188 | private function getXmlError(object $error, array $xml): string 189 | { 190 | $return = $xml[$error->line - 1] . "\n"; 191 | $return .= str_repeat('-', $error->column) . "^\n"; 192 | 193 | switch ($error->level) { 194 | case LIBXML_ERR_WARNING: 195 | $return .= "Warning $error->code: "; 196 | break; 197 | case LIBXML_ERR_ERROR: 198 | $return .= "Error $error->code: "; 199 | break; 200 | case LIBXML_ERR_FATAL: 201 | $return .= "Fatal Error $error->code: "; 202 | break; 203 | } 204 | 205 | $return .= trim($error->message) . 206 | "\n Line: $error->line" . 207 | "\n Column: $error->column"; 208 | 209 | if ($error->file) { 210 | $return .= "\n File: $error->file"; 211 | } 212 | 213 | return "$return\n\n--------------------------------------------\n\n"; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/Role.php: -------------------------------------------------------------------------------- 1 | getByProject($projectId); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Tag.php: -------------------------------------------------------------------------------- 1 | getResourceName($resource); 41 | 42 | return $this->fetch("$resource/$id/$this->action"); 43 | } 44 | 45 | /** 46 | * Remove Tags on a Resource 47 | * 48 | * @param string $resource 49 | * @param integer $id 50 | * @return bool 51 | */ 52 | 53 | public function removeTo(string $resource, int $id, int|string|array $data): bool 54 | { 55 | return $this->updateTo($resource, $id, $data, [ 56 | 'removeProvidedTags' => true 57 | ]); 58 | } 59 | 60 | /** 61 | * Add Tags on a Resource 62 | * 63 | * @param string $resource 64 | * @param integer $id 65 | * @return bool 66 | */ 67 | public function addTo(string $resource, int $id, int|string|array $data): bool 68 | { 69 | return $this->updateTo($resource, $id, $data); 70 | } 71 | 72 | private function updateTo(string $resource, int $id, int|string|array $data, iterable $opts = []): bool 73 | { 74 | $resource = $this->getResourceName($resource); 75 | if (is_array($data)) { 76 | if (is_array_of_int($data)) { 77 | $data = ['tagIds' => implode(',', $data)]; 78 | } else { 79 | $data = ['tags' => ['content' => implode('', $data)]]; 80 | } 81 | } else if (is_int($data)) { 82 | $data = ['tagIds' => $data]; 83 | } else { 84 | $data = ['tags' => ['content' => $data]]; 85 | } 86 | 87 | foreach ($opts as $key => $value) { 88 | $data[$key] = $value; 89 | } 90 | 91 | return $this 92 | ->notUseFields() 93 | ->put("$resource/$id/$this->action", $data); 94 | } 95 | 96 | private function getResourceName(string $resource): string 97 | { 98 | $resource = match ($resource) { 99 | 'people' => 'users', 100 | 'time_entries' => 'timelogs', 101 | default => $resource, 102 | }; 103 | if (!in_array($resource, static::RESOURCES, true)) { 104 | throw new Exception('Invalid resource type: ' . $resource); 105 | } 106 | return $resource; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Task.php: -------------------------------------------------------------------------------- 1 | fetch("tasklists/$id/$this->action", $params); 37 | } 38 | 39 | /** 40 | * Retrieve Task Dependencies 41 | * 42 | * @param int $id 43 | * @return Response 44 | * @throws Exception 45 | */ 46 | public function getDependencies(int $id): Response 47 | { 48 | return $this->fetch("$this->action/$id/dependencies"); 49 | } 50 | 51 | /** 52 | * Get completed Tasks 53 | * 54 | * @todo Need check 55 | * 56 | * @return Response 57 | * @throws Exception 58 | */ 59 | public function getCompleted(): Response 60 | { 61 | return $this->fetch("completedtasks"); 62 | } 63 | 64 | /** 65 | * Get Task Followers 66 | * 67 | * @param int $id 68 | * @return Response 69 | * @throws Exception 70 | */ 71 | public function getFollowers(int $id): Response 72 | { 73 | return $this->fetch("$this->action/$id/followers"); 74 | } 75 | 76 | /** 77 | * Get Task Predecessors 78 | * 79 | * @param int $id 80 | * @return Response 81 | * @throws Exception 82 | */ 83 | public function getPredecessors(int $id): Response 84 | { 85 | return $this->fetch("$this->action/$id/predecessors"); 86 | } 87 | 88 | /** 89 | * Create a Time-Entry (for a Task) 90 | * 91 | * @param integer $id 92 | * @param array|object $data 93 | * @return integer 94 | */ 95 | public function addTime(int $id, array|object $data): int 96 | { 97 | $data = arr_obj($data); 98 | $data['task_id'] = $id; 99 | 100 | return Factory::time()->create($data); 101 | } 102 | 103 | /** 104 | * Total Time on a Task 105 | * 106 | * @param int $id 107 | * @param array|object $params 108 | * @return Response 109 | * @throws Exception 110 | */ 111 | public function getTotalTime(int $id, array|object $params = []): Response 112 | { 113 | return $this->fetch("$this->action/$id/time/total", $params); 114 | } 115 | 116 | /** 117 | * Retrieve all Task times 118 | * 119 | * @param int $id 120 | * @return Response 121 | * @throws Exception 122 | */ 123 | public function getTimes(int $id): Response 124 | { 125 | return Factory::time()->getByTask($id); 126 | } 127 | 128 | /** 129 | * Add a Time estimate to a Task 130 | * 131 | * @param int $id 132 | * @param int $minutes 133 | * @return bool 134 | * @throws Exception 135 | */ 136 | public function addEstimateTime(int $id, int $minutes): bool 137 | { 138 | return $this 139 | ->notUseFields() 140 | ->put("$this->action/$id/estimatedtime", [ 141 | 'taskEstimatedMinutes' => $minutes 142 | ]); 143 | } 144 | 145 | /** 146 | * Get Sub Tasks of a Task 147 | * 148 | * @param int $id 149 | * @return Response 150 | * @throws Exception 151 | */ 152 | public function getSubTasks(int $id): Response 153 | { 154 | return $this->fetch("$this->action/$id/subtasks"); 155 | } 156 | 157 | /** 158 | * Get Recurring Tasks related to original Task. 159 | * 160 | * @param int $id 161 | * @param array|object $params 162 | * @return Response 163 | * @throws Exception 164 | */ 165 | public function getRecurring(int $id, array|object $params = []): Response 166 | { 167 | return $this->fetch("$this->action/$id/recurring", $params); 168 | } 169 | 170 | /** 171 | * Create a Task on a Project 172 | * Create a Task on a TaskList 173 | * 174 | * @param array|object $data 175 | * @return int 176 | * @throws Exception 177 | */ 178 | public function create(array|object $data): int 179 | { 180 | $data = arr_obj($data); 181 | 182 | $taskListId = $data->pull('task_list_id'); 183 | $projectId = $data->pull('project_id'); 184 | if ($projectId && $taskListId) { 185 | $data['task_list_id'] = $taskListId; 186 | } 187 | 188 | if (!($projectId || $taskListId)) { 189 | throw new Exception('Required field task_list_id or project_id'); 190 | } 191 | 192 | if ($projectId && $taskListId) { 193 | throw new Exception('Only one field task_list_id or project_id'); 194 | } 195 | 196 | $root = $projectId ? 'projects' : 'tasklists'; 197 | $id = $projectId ? $projectId : $taskListId; 198 | $files = $data->pull('files'); 199 | if ($files !== null) { 200 | $data['pending_file_attachments'] = Factory::file() 201 | ->upload($files); 202 | } 203 | 204 | return $this->post("$root/$id/$this->action", $data); 205 | } 206 | 207 | /** 208 | * Create a Sub Task 209 | * 210 | * @param integer $id 211 | * @param array|object $data 212 | * @return integer 213 | */ 214 | public function add(int $id, array|object $data): int 215 | { 216 | $data = arr_obj($data); 217 | $data->pull('project_id'); 218 | $data->pull('task_list_id'); 219 | $files = $data->pull('files'); 220 | if ($files !== null) { 221 | $data['pending_file_attachments'] = Factory::file() 222 | ->upload($files); 223 | } 224 | 225 | return $this->post("$this->action/$id", $data); 226 | } 227 | 228 | /** 229 | * Reorder the Tasks 230 | * 231 | * @param int $id 232 | * @param array $ids 233 | * @return bool 234 | * @throws Exception 235 | */ 236 | public function reorder(int $id, int ...$ids) 237 | { 238 | $params = []; 239 | foreach ($ids as $task) { 240 | $params[$this->parent][]['id'] = $task; 241 | } 242 | $parent = $this->parent . 's'; 243 | $params = [$parent => $params]; 244 | 245 | return $this->put("tasklists/$id/$this->action/reorder", $ids); 246 | } 247 | 248 | /** 249 | * Move a Task from one Project to Another 250 | * 251 | * @param integer $id 252 | * @param integer $projectId 253 | * @param integer $taskListId 254 | * @return Response 255 | */ 256 | public function move(int $id, int $projectId, int $taskListId): Response 257 | { 258 | return $this->notUseFields() 259 | ->put( 260 | "$this->action/$id/move", 261 | compact('projectId', 'taskListId') 262 | ); 263 | } 264 | 265 | /** 266 | * Copy a Task from one Project to Another 267 | * 268 | * @param integer $id 269 | * @param integer $projectId 270 | * @param integer $taskListId 271 | * @return int 272 | */ 273 | public function copy(int $id, int $projectId, int $taskListId): int 274 | { 275 | return $this->notUseFields() 276 | ->put( 277 | "$this->action/$id/copy", 278 | compact('projectId', 'taskListId') 279 | ); 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/Task/Custom/Field.php: -------------------------------------------------------------------------------- 1 | fetch("tasks/$id/$this->action"); 33 | } 34 | 35 | /** 36 | * Add a File to a Task 37 | * 38 | * @param integer $id 39 | * @param object|array $data 40 | * @return int 41 | * @throws Exception 42 | */ 43 | public function add(int $id, object|array $data): int 44 | { 45 | return $this->post("tasks/$id/$this->action", $data); 46 | } 47 | } -------------------------------------------------------------------------------- /src/Task_List.php: -------------------------------------------------------------------------------- 1 | fetch("$this->action/templates"); 33 | } 34 | 35 | /** 36 | * Total Time on a Tasklist 37 | * 38 | * @param int $id 39 | * @param array|object $params 40 | * @return Response 41 | * @throws Exception 42 | */ 43 | public function getTotalTime(int $id, array|object $params = []): Response 44 | { 45 | return $this->fetch("$this->action/$id/time/total", $params); 46 | } 47 | 48 | /** 49 | * Reorder Lists 50 | * 51 | * @param int $projectId 52 | * @param array $ids 53 | * @return bool 54 | */ 55 | public function reorder(int $projectId, int ...$ids): bool 56 | { 57 | $params = []; 58 | foreach ($ids as $id) { 59 | $params[$this->parent][]['id'] = $id; 60 | } 61 | $parent = $this->parent . 's'; 62 | $params = [$parent => $params]; 63 | 64 | return $this 65 | ->notUseFields() 66 | ->put( 67 | "projects/$projectId/$this->action/reorder", 68 | $params 69 | ); 70 | } 71 | 72 | /** 73 | * @param array $data 74 | * 75 | * @return int 76 | * @throws Exception 77 | */ 78 | public function create(object|array $data): int 79 | { 80 | $data = arr_obj($data); 81 | $projectId = $data->pull('project_id'); 82 | $this->validates([ 83 | 'project_id' => $projectId 84 | ]); 85 | $apply_defaults_to_existing_tasks = $data->pull( 86 | 'apply_defaults_to_existing_tasks' 87 | ); 88 | $data = [ 89 | $this->parent => $data 90 | ]; 91 | if ($apply_defaults_to_existing_tasks !== null) { 92 | $data['apply_defaults_to_existing_tasks'] = $apply_defaults_to_existing_tasks; 93 | } 94 | 95 | return $this->post("projects/$projectId/$this->action", $data); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Team.php: -------------------------------------------------------------------------------- 1 | pull('project_id'); 30 | $taskId = (int) $data->pull('task_id'); 31 | if (!($taskId || $projectId)) { 32 | throw new Exception('Required field project_id or task_id'); 33 | } 34 | 35 | if ($taskId && $projectId) { 36 | throw new Exception('Only one field project_id or task_id'); 37 | } 38 | 39 | if (!($data->offsetExists('hours') || $data->offsetExists('minutes'))) { 40 | throw new Exception('Required field hours or minutes'); 41 | } 42 | 43 | $path = "projects/$projectId"; 44 | if ($taskId) { 45 | $path = "tasks/$taskId"; 46 | } 47 | 48 | return $this->post("$path/$this->action", $data); 49 | } 50 | 51 | /** 52 | * Time Totals on a Project 53 | * 54 | * @param int $id 55 | * @param array|object $params 56 | * @return Response 57 | * @throws Exception 58 | */ 59 | public function getTotal(array|object $params = []): Response 60 | { 61 | return $this->fetch("$this->action/total", $params); 62 | } 63 | 64 | /** 65 | * Estimated Time Totals on Projects 66 | * 67 | * @param array|object $params 68 | * @return Response 69 | * @throws Exception 70 | */ 71 | public function getEstimated(array|object $params = []): Response 72 | { 73 | return $this->fetch("projects/estimatedtime/total", $params); 74 | } 75 | 76 | public function getTimers(array|object $params = []): Response 77 | { 78 | return $this->fetch('timers', $params); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Timezone.php: -------------------------------------------------------------------------------- 1 | fetch("$this->action/projects/$projectId"); 45 | } 46 | 47 | /** 48 | * Restore Resource Items from the Trashcan 49 | * 50 | * @param string $resource 51 | * @param integer $id 52 | * @return boolean 53 | */ 54 | public function restore(string $resource, int $id): bool 55 | { 56 | if (!in_array($resource, static::RESOURCES, true)) { 57 | throw new Exception('Invalid resource name: ' . $resource); 58 | } 59 | 60 | return $this->notUseFields() 61 | ->put("$this->action/$resource/$id/restore"); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Workload.php: -------------------------------------------------------------------------------- 1 | $val) { 99 | /** 100 | * @var mixed 101 | */ 102 | $acc = $callback($acc, $val, $key); 103 | } 104 | 105 | return $acc; 106 | } 107 | } 108 | 109 | /** 110 | * @template TKey of array-key 111 | * @template TValue 112 | * @extends \ArrayObject 113 | */ 114 | class ArrayObject extends \ArrayObject 115 | { 116 | public function toArray(): array 117 | { 118 | $array = []; 119 | foreach ($this->getArrayCopy() as $key => $value) { 120 | /** @disregard */ 121 | $array[$key] = $value instanceof static ? $value->toArray() : $value; 122 | } 123 | 124 | return $array; 125 | } 126 | 127 | public function reduce(callable $callback, mixed $initial = []): mixed 128 | { 129 | /** 130 | * @var mixed 131 | */ 132 | $accumulator = $initial; 133 | foreach ($this as $key => $value) { 134 | /** 135 | * @var mixed 136 | */ 137 | $accumulator = $callback($accumulator, $value, $key); 138 | } 139 | 140 | return $accumulator; 141 | } 142 | 143 | public function __debugInfo(): array 144 | { 145 | return $this->toArray(); 146 | } 147 | 148 | /** 149 | * 150 | * @param TKey $key 151 | * @return mixed 152 | */ 153 | public function offsetGet(mixed $key): mixed 154 | { 155 | return $this->offsetExists($key) ? 156 | parent::offsetGet($key) : null; 157 | } 158 | 159 | /** 160 | * 161 | * @param TKey $key 162 | * @return void 163 | */ 164 | public function offsetUnset(mixed $key): void 165 | { 166 | if ($this->offsetExists($key)) { 167 | parent::offsetUnset($key); 168 | } 169 | } 170 | 171 | /** 172 | * 173 | * @param TKey $key 174 | * @return mixed 175 | */ 176 | public function pull(mixed $key): mixed 177 | { 178 | if (!$this->offsetExists($key)) { 179 | return null; 180 | } 181 | /** 182 | * @var mixed 183 | */ 184 | $value = $this->offsetGet($key); 185 | $this->offsetUnset($key); 186 | 187 | return $value; 188 | } 189 | 190 | public function has(): bool 191 | { 192 | return count($this) > 0; 193 | } 194 | } 195 | } 196 | --------------------------------------------------------------------------------