├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── config └── asana.php ├── phpunit.xml ├── src ├── Asana.php ├── AsanaCurl.php ├── Facade │ └── Asana.php ├── ServiceProvider.php └── helpers.php └── tests └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store 5 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.3 5 | - 7.4 6 | - 8.0 7 | 8 | before_script: 9 | - curl -s http://getcomposer.org/installer | php 10 | - php composer.phar install --dev 11 | 12 | script: phpunit 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The BSD 2-Clause License 2 | Copyright (c) 2013-2018, Daniel Stainback 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Asana for Laravel 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/torann/laravel-asana/v/stable)](https://packagist.org/packages/torann/laravel-asana) 4 | [![Total Downloads](https://poser.pugx.org/torann/laravel-asana/downloads)](https://packagist.org/packages/torann/laravel-asana) 5 | [![Patreon donate button](https://img.shields.io/badge/patreon-donate-yellow.svg)](https://www.patreon.com/torann) 6 | [![Donate to this project using Paypal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=4CJA2A97NPYVU) 7 | 8 | Asana API wrapper for Laravel, supporting workspaces, projects, tasks, tags, users and stories. 9 | 10 | - [Laravel Asana on Packagist](https://packagist.org/packages/torann/laravel-asana) 11 | - [Laravel Asana on GitHub](https://github.com/torann/laravel-asana) 12 | 13 | ## Official Documentation 14 | 15 | Documentation for the package can be found on [Lyften.com](http://lyften.com/projects/laravel-asana/). 16 | 17 | ## Change Log 18 | 19 | #### v0.4.0 20 | 21 | - Added attachment 22 | - Code cleanup 23 | - Add helper function 24 | 25 | #### v0.3.1 26 | 27 | - Fix big integers parsing 28 | - Fix `getTasksByFilter` ignoring filters beside the predefined ones 29 | 30 | #### v0.3.0 31 | 32 | - Remove API key (deprecated) support 33 | 34 | #### v0.2.1 35 | 36 | - Add support for Lumen 37 | - Code cleanup 38 | 39 | #### v0.2.0 40 | 41 | - Update to Laravel 5 42 | 43 | #### v0.1.1 44 | 45 | - Code cleanup 46 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "torann/laravel-asana", 3 | "description": "Asana API wrapper for Laravel", 4 | "keywords": [ 5 | "laravel", 6 | "asana" 7 | ], 8 | "license": "BSD-2-Clause", 9 | "authors": [ 10 | { 11 | "name": "Daniel Stainback", 12 | "email": "torann@gmail.com" 13 | } 14 | ], 15 | "require": { 16 | "php": ">=5.5.9", 17 | "illuminate/support": "^5.5|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0" 18 | }, 19 | "autoload": { 20 | "files": [ 21 | "src/helpers.php" 22 | ], 23 | "psr-4": { 24 | "Torann\\LaravelAsana\\": "src/" 25 | } 26 | }, 27 | "extra": { 28 | "branch-alias": { 29 | "dev-master": "0.4-dev" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /config/asana.php: -------------------------------------------------------------------------------- 1 | env('ASANA_TOKEN'), 13 | 14 | /* 15 | |-------------------------------------------------------------------------- 16 | | Asana default workspace 17 | |-------------------------------------------------------------------------- 18 | | 19 | */ 20 | 21 | 'workspaceId' => '', 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Asana default project 26 | |-------------------------------------------------------------------------- 27 | | 28 | */ 29 | 30 | 'projectId' => '', 31 | 32 | ]; 33 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Asana.php: -------------------------------------------------------------------------------- 1 | curl = new AsanaCurl($config['accessToken']); 41 | 42 | // Set defaults 43 | $this->defaultWorkspaceId = $config['workspaceId']; 44 | $this->defaultProjectId = $config['projectId']; 45 | } 46 | 47 | /** 48 | * Returns the full user record for a single user. 49 | * 50 | * @param string $userId 51 | * 52 | * @return string|null 53 | */ 54 | public function getUserInfo($userId = null) 55 | { 56 | return $this->curl->get("users/{$userId}"); 57 | } 58 | 59 | /** 60 | * Returns the full user record for the current user. 61 | * 62 | * @return string|null 63 | */ 64 | public function getCurrentUser() 65 | { 66 | return $this->curl->get('users/me'); 67 | } 68 | 69 | /** 70 | * Returns the user records for all users in all workspaces you have access. 71 | * 72 | * @return string|null 73 | */ 74 | public function getUsers($opt_fields = null) 75 | { 76 | $url = $opt_fields ? 'users?opt_fields=' . $opt_fields : 'users'; 77 | return $this->curl->get($url); 78 | } 79 | 80 | /** 81 | * Function to create a task. 82 | * 83 | * For assign or remove the task to a project, use the addProjectToTask and removeProjectToTask. 84 | * 85 | * @param array $data 86 | * 87 | * Example: 88 | * 89 | * array( 90 | * "workspace" => "1768", 91 | * "name" => "Hello World!", 92 | * "notes" => "This is a task for testing the Asana API :)", 93 | * "assignee" => "176822166183", 94 | * "followers" => array( 95 | * "37136", 96 | * "59083" 97 | * ) 98 | * ) 99 | * 100 | * @return object|null 101 | */ 102 | public function createTask($data) 103 | { 104 | $data = array_merge([ 105 | 'workspace' => $this->defaultWorkspaceId, 106 | 'projects' => $this->defaultProjectId 107 | ], $data); 108 | 109 | return $this->curl->post('tasks', ['data' => $data]); 110 | } 111 | 112 | /** 113 | * Returns task information 114 | * 115 | * @param string $taskId 116 | * 117 | * @return string|null 118 | */ 119 | public function getTask($taskId) 120 | { 121 | return $this->curl->get("tasks/{$taskId}"); 122 | } 123 | 124 | /** 125 | * Returns sub-task information 126 | * 127 | * @param string $taskId 128 | * 129 | * @return string|null 130 | */ 131 | public function getSubTasks($taskId) 132 | { 133 | return $this->curl->get("tasks/{$taskId}/subtasks"); 134 | } 135 | 136 | /** 137 | * Updates a task 138 | * 139 | * @param string $taskId 140 | * @param array $data 141 | * 142 | * @return string|null 143 | */ 144 | public function updateTask($taskId, $data) 145 | { 146 | return $this->curl->put("tasks/{$taskId}", ['data' => $data]); 147 | } 148 | 149 | /** 150 | * Delete a task 151 | * 152 | * @param string $taskId 153 | * 154 | * @return string|null 155 | */ 156 | public function deleteTask($taskId) 157 | { 158 | return $this->curl->delete("tasks/{$taskId}"); 159 | } 160 | 161 | /** 162 | * Add Attachment to a task 163 | * 164 | * @param string $taskId 165 | * @param array $file 166 | * 167 | * @return string|null 168 | */ 169 | public function addTaskAttachment($taskId, $file) 170 | { 171 | return $this->curl->post("tasks/{$taskId}/attachments", [ 172 | 'file' => $file, 173 | ]); 174 | } 175 | 176 | /** 177 | * getAllAttachments 178 | * 179 | * Gets a List of all available Attachments. 180 | * 181 | * @param $taskId 182 | * 183 | * @return null|string 184 | * @author Olly Warren, Big Bite Creative 185 | * @package Torann\LaravelAsana 186 | * @version 1.0 187 | */ 188 | public function getAllAttachments($taskId) 189 | { 190 | return $this->curl->get("tasks/{$taskId}/attachments"); 191 | } 192 | 193 | /** 194 | * getSingleAttachment 195 | * 196 | * Gets a Single Attachment based on a file id. 197 | * 198 | * @param $attachmentId 199 | * 200 | * @return null|string 201 | * @author Olly Warren, Big Bite Creative 202 | * @package Torann\LaravelAsana 203 | * @version 1.0 204 | */ 205 | public function getSingleAttachment($attachmentId) 206 | { 207 | return $this->curl->get("attachments/{$attachmentId}"); 208 | } 209 | 210 | /** 211 | * Returns the projects associated to the task. 212 | * 213 | * @param string $taskId 214 | * 215 | * @return string|null 216 | */ 217 | public function getProjectsForTask($taskId) 218 | { 219 | return $this->curl->get("tasks/{$taskId}/projects"); 220 | } 221 | 222 | /** 223 | * Adds a project to task. If successful, will 224 | * return success and an empty data block. 225 | * 226 | * @param string $taskId 227 | * @param string $projectId 228 | * 229 | * @return string|null 230 | */ 231 | public function addProjectToTask($taskId, $projectId = null) 232 | { 233 | $data = [ 234 | 'project' => $projectId ?: $this->defaultProjectId 235 | ]; 236 | 237 | return $this->curl->post("tasks/{$taskId}/addProject", ['data' => $data]); 238 | } 239 | 240 | /** 241 | * Removes project from task. If successful, will 242 | * return success and an empty data block. 243 | * 244 | * @param string $taskId 245 | * @param string $projectId 246 | * 247 | * @return string|null 248 | */ 249 | public function removeProjectToTask($taskId, $projectId = null) 250 | { 251 | $data = [ 252 | 'project' => $projectId ?: $this->defaultProjectId 253 | ]; 254 | 255 | return $this->curl->post("tasks/{$taskId}/removeProject", ['data' => $data]); 256 | } 257 | 258 | /** 259 | * Returns task by a given filter. 260 | * 261 | * For now (limited by Asana API), you may limit your 262 | * query either to a specific project or to an assignee and workspace 263 | * 264 | * NOTE: As Asana API says, if you filter by assignee, you MUST specify a workspaceId and vice-a-versa. 265 | * 266 | * @param array $filter 267 | * 268 | * array( 269 | * "assignee" => "", 270 | * "project" => 0, 271 | * "workspace" => 0 272 | * ) 273 | * 274 | * @return string|null 275 | */ 276 | public function getTasksByFilter($filter = ["assignee" => "", "project" => "", "workspace" => ""]) 277 | { 278 | $filter = array_filter(array_merge(["assignee" => "", "project" => "", "workspace" => ""], $filter)); 279 | $url = '?' . http_build_query($filter); 280 | 281 | return $this->curl->get("tasks{$url}"); 282 | } 283 | 284 | /** 285 | * Returns the list of stories associated with the object. 286 | * As usual with queries, stories are returned in compact form. 287 | * However, the compact form for stories contains more information by default than just the ID. 288 | * 289 | * There is presently no way to get a filtered set of stories. 290 | * 291 | * @param string $taskId 292 | * 293 | * @return string|null 294 | */ 295 | public function getTaskStories($taskId) 296 | { 297 | return $this->curl->get("tasks/{$taskId}/stories"); 298 | } 299 | 300 | /** 301 | * Adds a comment to a task. 302 | * 303 | * The comment will be authored by the authorized user, and 304 | * timestamped when the server receives the request. 305 | * 306 | * @param string $taskId 307 | * @param string $text 308 | * 309 | * @return string|null 310 | */ 311 | public function commentOnTask($taskId, $text = "") 312 | { 313 | $data = [ 314 | 'text' => $text 315 | ]; 316 | 317 | return $this->curl->post("tasks/{$taskId}/stories", ['data' => $data]); 318 | } 319 | 320 | /** 321 | * Adds a tag to a task. If successful, will return success and an empty data block. 322 | * 323 | * @param string $taskId 324 | * @param string $tagId 325 | * 326 | * @return string|null 327 | */ 328 | public function addTagToTask($taskId, $tagId) 329 | { 330 | $data = [ 331 | "tag" => $tagId 332 | ]; 333 | 334 | return $this->curl->post("tasks/{$taskId}/addTag", ['data' => $data]); 335 | } 336 | 337 | /** 338 | * Removes a tag from a task. If successful, will return success and an empty data block. 339 | * 340 | * @param string $taskId 341 | * @param string $tagId 342 | * 343 | * @return string|null 344 | */ 345 | public function removeTagFromTask($taskId, $tagId) 346 | { 347 | $data = [ 348 | "tag" => $tagId 349 | ]; 350 | 351 | return $this->curl->post("tasks/{$taskId}/removeTag", ['data' => $data]); 352 | } 353 | 354 | /** 355 | * Function to create a project. 356 | * 357 | * @param array $data Array of data for the project following the Asana API documentation. 358 | * 359 | * Example: 360 | * 361 | * array( 362 | * "workspace" => "1768", 363 | * "name" => "Foo Project!", 364 | * "notes" => "This is a test project" 365 | * ) 366 | * 367 | * @return string|null 368 | */ 369 | public function createProject($data) 370 | { 371 | return $this->curl->post('projects', ['data' => $data]); 372 | } 373 | 374 | /** 375 | * Returns the full record for a single project. 376 | * 377 | * @param string $projectId 378 | * 379 | * @return string|null 380 | */ 381 | public function getProject($projectId = null) 382 | { 383 | $projectId = $projectId ?: $this->defaultProjectId; 384 | 385 | return $this->curl->get("projects/{$projectId}"); 386 | } 387 | 388 | /** 389 | * Returns the projects in all workspaces containing archived ones or not. 390 | * 391 | * @param boolean $archived Return archived projects or not 392 | * @param string $opt_fields Return results with optional parameters 393 | * 394 | * @return string JSON or null 395 | */ 396 | public function getProjects($archived = false, $opt_fields = "") 397 | { 398 | $archived = $archived ? "true" : "false"; 399 | $opt_fields = ($opt_fields != "") ? "&opt_fields={$opt_fields}" : ""; 400 | 401 | return $this->curl->get("projects?archived={$archived}{$opt_fields}"); 402 | } 403 | 404 | /** 405 | * Returns the projects in provided workspace containing archived ones or not. 406 | * 407 | * @param string $workspaceId 408 | * @param boolean $archived Return archived projects or not 409 | * 410 | * @return string JSON or null 411 | */ 412 | public function getProjectsInWorkspace($workspaceId = null, $archived = false) 413 | { 414 | $archived = $archived ? 1 : 0; 415 | $workspaceId = $workspaceId ?: $this->defaultWorkspaceId; 416 | 417 | return $this->curl->get("projects?archived={$archived}&workspace={$workspaceId}"); 418 | } 419 | 420 | /** 421 | * This method modifies the fields of a project provided 422 | * in the request, then returns the full updated record. 423 | * 424 | * @param string $projectId 425 | * @param array $data 426 | * 427 | * @return string|null 428 | */ 429 | public function updateProject($projectId = null, $data) 430 | { 431 | $projectId = $projectId ?: $this->defaultProjectId; 432 | 433 | return $this->curl->put("projects/{$projectId}", ['data' => $data]); 434 | } 435 | 436 | /** 437 | * Returns all unarchived tasks of a given project 438 | * 439 | * @param string $projectId 440 | * 441 | * @return string|null 442 | */ 443 | public function getProjectTasks($projectId = null) 444 | { 445 | $projectId = $projectId ?: $this->defaultProjectId; 446 | 447 | return $this->curl->get("tasks?project={$projectId}"); 448 | } 449 | 450 | /** 451 | * Returns the list of stories associated with the object. 452 | * As usual with queries, stories are returned in compact form. 453 | * However, the compact form for stories contains more 454 | * information by default than just the ID. 455 | * There is presently no way to get a filtered set of stories. 456 | * 457 | * @param string $projectId 458 | * 459 | * @return string|null 460 | */ 461 | public function getProjectStories($projectId = null) 462 | { 463 | $projectId = $projectId ?: $this->defaultProjectId; 464 | 465 | return $this->curl->get("projects/{$projectId}/stories"); 466 | } 467 | 468 | /** 469 | * Adds a comment to a project 470 | * 471 | * The comment will be authored by the authorized user, and 472 | * timestamped when the server receives the request. 473 | * 474 | * @param string $projectId 475 | * @param string $text 476 | * 477 | * @return string|null 478 | */ 479 | public function commentOnProject($projectId = null, $text = "") 480 | { 481 | $projectId = $projectId ?: $this->defaultProjectId; 482 | 483 | $data = [ 484 | "text" => $text 485 | ]; 486 | 487 | return $this->curl->post("projects/{$projectId}/stories", ['data' => $data]); 488 | } 489 | 490 | /** 491 | * Returns the full record for a single tag. 492 | * 493 | * @param string $tagId 494 | * 495 | * @return string|null 496 | */ 497 | public function getTag($tagId) 498 | { 499 | return $this->curl->get("tags/{$tagId}"); 500 | } 501 | 502 | /** 503 | * Returns the full record for all tags in all workspaces. 504 | * 505 | * @return string|null 506 | */ 507 | public function getTags() 508 | { 509 | return $this->curl->get('tags'); 510 | } 511 | 512 | /** 513 | * Modifies the fields of a tag provided in the request, 514 | * then returns the full updated record. 515 | * 516 | * @param string $tagId 517 | * @param array $data 518 | * 519 | * @return string|null 520 | */ 521 | public function updateTag($tagId, $data) 522 | { 523 | return $this->curl->put("tags/{$tagId}", ['data' => $data]); 524 | } 525 | 526 | /** 527 | * Returns the list of all tasks with this tag. Tasks 528 | * can have more than one tag at a time. 529 | * 530 | * @param string $tagId 531 | * 532 | * @return string|null 533 | */ 534 | public function getTasksWithTag($tagId) 535 | { 536 | return $this->curl->get("tags/{$tagId}/tasks"); 537 | } 538 | 539 | /** 540 | * Returns the full record for a single story. 541 | * 542 | * @param string $storyId 543 | * 544 | * @return string|null 545 | */ 546 | public function getSingleStory($storyId) 547 | { 548 | return $this->curl->get("stories/{$storyId}"); 549 | } 550 | 551 | /** 552 | * Returns all the workspaces. 553 | * 554 | * @return string|null 555 | */ 556 | public function getWorkspaces() 557 | { 558 | return $this->curl->get('workspaces'); 559 | } 560 | 561 | /** 562 | * Currently the only field that can be modified for a 563 | * workspace is its name (as Asana API says). 564 | * 565 | * This method returns the complete updated workspace record. 566 | * 567 | * @param array $data 568 | * 569 | * @return string|null 570 | */ 571 | public function updateWorkspace($workspaceId = null, $data = ["name" => ""]) 572 | { 573 | $workspaceId = $workspaceId ?: $this->defaultWorkspaceId; 574 | 575 | return $this->curl->put("workspaces/{$workspaceId}", ['data' => $data]); 576 | } 577 | 578 | /** 579 | * Returns tasks of all workspace assigned to someone. 580 | * 581 | * Note: As Asana API says, you must specify an assignee when querying for workspace tasks. 582 | * 583 | * @param string $workspaceId The id of the workspace 584 | * @param string $assignee Can be "me" or user ID 585 | * 586 | * @return string|null 587 | */ 588 | public function getWorkspaceTasks($workspaceId = null, $assignee = "me") 589 | { 590 | $workspaceId = $workspaceId ?: $this->defaultWorkspaceId; 591 | 592 | return $this->curl->get("tasks?workspace={$workspaceId}&assignee={$assignee}"); 593 | } 594 | 595 | /** 596 | * Returns tags of all workspace. 597 | * 598 | * @param string $workspaceId The id of the workspace 599 | * 600 | * @return string|null 601 | */ 602 | public function getWorkspaceTags($workspaceId = null) 603 | { 604 | $workspaceId = $workspaceId ?: $this->defaultWorkspaceId; 605 | 606 | return $this->curl->get("workspaces/{$workspaceId}/tags"); 607 | } 608 | 609 | /** 610 | * Returns users of all workspace. 611 | * 612 | * @param string $workspaceId The id of the workspace 613 | * 614 | * @return string|null 615 | */ 616 | public function getWorkspaceUsers($workspaceId = null) 617 | { 618 | $workspaceId = $workspaceId ?: $this->defaultWorkspaceId; 619 | 620 | return $this->curl->get("workspaces/{$workspaceId}/users"); 621 | } 622 | 623 | /** 624 | * Returns events of a given project 625 | * 626 | * @param string $projectId The id of the project 627 | * 628 | * @return string|null 629 | */ 630 | public function getProjectEvents($projectId = null) 631 | { 632 | $projectId = $projectId ?: $this->defaultProjectId; 633 | 634 | return $this->curl->get("projects/{$projectId}/events"); 635 | } 636 | 637 | /** 638 | * Return event sync key 639 | * 640 | * @return mixed 641 | */ 642 | public function getSyncKey() 643 | { 644 | return $this->curl->getSyncKey(); 645 | } 646 | 647 | /** 648 | * Return error 649 | * 650 | * @return mixed 651 | */ 652 | public function getErrors() 653 | { 654 | return $this->curl->getErrors(); 655 | } 656 | 657 | /** 658 | * getCustomFields 659 | * 660 | * Returns tall custom fields for a workspace 661 | * 662 | * @param $workspaceId 663 | * 664 | * @return null|string 665 | * @author Olly Warren https://github.com/ollywarren 666 | * @version 1.0 667 | */ 668 | public function getCustomFields($workspaceId = null) 669 | { 670 | $workspaceId = $workspaceId ?: $this->defaultWorkspaceId; 671 | 672 | return $this->curl->get("workspaces/{$workspaceId}/custom_fields"); 673 | } 674 | 675 | /** 676 | * getCustomField 677 | * 678 | * Returns the full details on the custom field passed in. 679 | * 680 | * @param $fieldId 681 | * 682 | * @return null|string 683 | * @author Olly Warren https://github.com/ollywarren 684 | * @version 1.0 685 | */ 686 | public function getCustomField($fieldId) 687 | { 688 | return $this->curl->get("custom_fields/{$fieldId}"); 689 | } 690 | 691 | /** 692 | * createWebhook 693 | * 694 | * Creates a webhook with asana based 695 | * Requires the resource to link with (Workspace, Project) 696 | * and a Target URL for your API/Application. 697 | * 698 | * Note: Will send a handshake to your Application with a 699 | * X-Security-Header that must be returned with a 200 700 | * Response to verify the webhook creation. Asana may then 701 | * follow up with a "heartbeat" request that will contain an 702 | * empty "events" JSON object and a X-Signature-Header. 703 | * 704 | * @param $resourceId 705 | * @param $targetUrl 706 | * 707 | * @return null|string 708 | * @author Olly Warren https://github.com/ollywarren 709 | * @version 1.0 710 | */ 711 | public function createWebhook($resourceId, $targetUrl) 712 | { 713 | //Define the Data array to include in the request 714 | $data = [ 715 | 'data' => [ 716 | 'resource' => $resourceId, 717 | 'target' => $targetUrl 718 | ] 719 | ]; 720 | 721 | return $this->curl->post("webhooks", $data); 722 | } 723 | 724 | /** 725 | * getWebhook 726 | * 727 | * Gets the full details for a Webhook. 728 | * 729 | * @param $webhookId 730 | * 731 | * @return null|string 732 | * @author Olly Warren https://github.com/ollywarren 733 | * @version 1.0 734 | */ 735 | public function getWebhook($webhookId) 736 | { 737 | return $this->curl->get("webhooks/{$webhookId}"); 738 | } 739 | 740 | /** 741 | * getWebhooks 742 | * 743 | * Gets all the webhooks for a workspace 744 | * 745 | * @param $workspaceId 746 | * 747 | * @return null|string 748 | * @author Olly Warren https://github.com/ollywarren 749 | * @version 1.0 750 | */ 751 | public function getWebhooks($workspaceId) 752 | { 753 | return $this->curl->get("webhooks?workspace={$workspaceId}"); 754 | } 755 | 756 | 757 | 758 | /** 759 | * deleteWebhook 760 | * 761 | * Removes a webhook from Asana. 762 | * 763 | * @param $webhookId 764 | * 765 | * @return null|string 766 | * @author Olly Warren https://github.com/ollywarren 767 | * @version 1.0 768 | */ 769 | public function deleteWebhook($webhookId) 770 | { 771 | return $this->curl->delete("webhooks/{$webhookId}"); 772 | } 773 | 774 | /** 775 | * addFollowersToTask 776 | * 777 | * @param $taskId 778 | * @param $followerIds 779 | * 780 | * @return string|null 781 | */ 782 | public function addFollowersFromTask($taskId, $followerIds) 783 | { 784 | return $this->curl->post("tasks/$taskId/addFollowers", ["data" => ["followers" => $followerIds]]); 785 | } 786 | 787 | /** 788 | * removeFollowersFromTask 789 | * 790 | * @param $taskId 791 | * @param $followerIds 792 | * 793 | * @return string|null 794 | */ 795 | public function removeFollowersFromTask($taskId, $followerIds) 796 | { 797 | return $this->curl->post("tasks/$taskId/removeFollowers", ["data" => ["followers" => $followerIds]]); 798 | } 799 | } 800 | -------------------------------------------------------------------------------- /src/AsanaCurl.php: -------------------------------------------------------------------------------- 1 | 'Accept: application/json', 45 | ]; 46 | 47 | /** 48 | * Constructor 49 | * 50 | * @param string $token 51 | * 52 | * @throws Exception 53 | */ 54 | public function __construct($token = null) 55 | { 56 | if (empty($token)) { 57 | throw new Exception('You need to specify an access token.'); 58 | } 59 | 60 | $this->setHeader('Authorization', "Bearer {$token}"); 61 | } 62 | 63 | /** 64 | * Add multiple headers to request. 65 | * 66 | * @param array $values 67 | */ 68 | public function setHeaders(array $values) 69 | { 70 | foreach ($values as $key => $value) { 71 | $this->setHeader($key, $value); 72 | } 73 | } 74 | 75 | /** 76 | * Add header to request. 77 | * 78 | * @param string $key 79 | * @param string $value 80 | */ 81 | public function setHeader($key, $value) 82 | { 83 | $this->headers[$key] = $value ? "{$key}: {$value}" : $value; 84 | } 85 | 86 | /** 87 | * Get request 88 | * 89 | * @param string $url 90 | * 91 | * @return string|null 92 | */ 93 | public function get($url) 94 | { 95 | return $this->request(self::METHOD_GET, $url); 96 | } 97 | 98 | /** 99 | * Post request 100 | * 101 | * @param string $url 102 | * @param array $data 103 | * 104 | * @return string|null 105 | */ 106 | public function post($url, array $data = []) 107 | { 108 | return $this->request(self::METHOD_POST, $url, $data); 109 | } 110 | 111 | /** 112 | * Put request 113 | * 114 | * @param string $url 115 | * @param array $data 116 | * 117 | * @return string|null 118 | */ 119 | public function put($url, array $data = []) 120 | { 121 | return $this->request(self::METHOD_PUT, $url, $data); 122 | } 123 | 124 | /** 125 | * Delete request 126 | * 127 | * @param string $url 128 | * @param array $data 129 | * 130 | * @return string|null 131 | */ 132 | public function delete($url, array $data = []) 133 | { 134 | return $this->request(self::METHOD_DELETE, $url, $data); 135 | } 136 | 137 | /** 138 | * Return error 139 | * 140 | * @return mixed 141 | */ 142 | public function getErrors() 143 | { 144 | return $this->errors; 145 | } 146 | 147 | /** 148 | * Return sync key 149 | * 150 | * @return mixed 151 | */ 152 | public function getSyncKey() 153 | { 154 | return $this->syncKey; 155 | } 156 | 157 | /** 158 | * This function communicates with Asana REST API. 159 | * You don't need to call this function directly. It's only for inner class working. 160 | * 161 | * @param int $method 162 | * @param string $url 163 | * @param array $parameters 164 | * @param array $headers 165 | * 166 | * @return string|null 167 | * @throws Exception 168 | */ 169 | private function request($method, $url, array $parameters = [], array $headers = []) 170 | { 171 | $this->errors = null; 172 | 173 | // Set default content type 174 | $this->setHeader('Content-Type', 'application/json'); 175 | 176 | $curl = curl_init(); 177 | 178 | // Set options 179 | curl_setopt_array($curl, [ 180 | CURLOPT_URL => "{$this->endpoint}{$url}", 181 | CURLOPT_CONNECTTIMEOUT => 10, 182 | CURLOPT_TIMEOUT => 90, 183 | CURLOPT_RETURNTRANSFER => 1, 184 | CURLOPT_SSL_VERIFYPEER => 0, 185 | CURLOPT_SSL_VERIFYHOST => 0, 186 | CURLOPT_HEADER => 1, 187 | CURLINFO_HEADER_OUT => 1, 188 | CURLOPT_VERBOSE => 1, 189 | ]); 190 | 191 | // Setup method specific options 192 | switch ($method) { 193 | case 'PUT': 194 | case 'PATCH': 195 | case 'POST': 196 | curl_setopt_array($curl, [ 197 | CURLOPT_CUSTOMREQUEST => $method, 198 | CURLOPT_POST => true, 199 | CURLOPT_POSTFIELDS => $this->buildArrayForCurl($parameters), 200 | ]); 201 | break; 202 | 203 | case 'DELETE': 204 | curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE'); 205 | break; 206 | 207 | default: 208 | curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'GET'); 209 | break; 210 | } 211 | 212 | // Set request headers 213 | curl_setopt($curl, CURLOPT_HTTPHEADER, array_filter(array_values($this->headers))); 214 | 215 | // Make request 216 | $response = curl_exec($curl); 217 | 218 | // Set HTTP response code 219 | $this->http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE); 220 | 221 | // Set errors if there are any 222 | if (curl_errno($curl)) { 223 | $this->errors = curl_error($curl); 224 | } 225 | 226 | // Parse body 227 | $header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE); 228 | $header = substr($response, 0, $header_size); 229 | $json = json_decode(substr($response, $header_size), false, 512, JSON_BIGINT_AS_STRING); 230 | 231 | // Check for errors 232 | $this->checkForCurlErrors($json); 233 | 234 | curl_close($curl); 235 | 236 | return $json; 237 | } 238 | 239 | /** 240 | * Build http query that will be cUrl compliant. 241 | * 242 | * @param array $params 243 | * 244 | * @return array 245 | */ 246 | protected function buildArrayForCurl($params) 247 | { 248 | if (isset($params['file'])) { 249 | 250 | // Have cUrl set the correct content type for upload 251 | $this->setHeader('Content-Type', null); 252 | 253 | // Convert array to a simple cUrl usable array 254 | return $this->http_build_query_for_curl($params); 255 | } 256 | 257 | return json_encode($params); 258 | } 259 | 260 | /** 261 | * Handle nested arrays when posting 262 | * 263 | * @param mixed $var 264 | * @param string $prefix 265 | * 266 | * @return array 267 | */ 268 | protected function http_build_query_for_curl($var, $prefix = null) 269 | { 270 | $return = []; 271 | 272 | foreach ($var as $key => $value) { 273 | $name = $prefix ? $prefix . '[' . $key . ']' : $key; 274 | 275 | if (is_array($value)) { 276 | $return = array_merge($return, $this->http_build_query_for_curl($value, $name)); 277 | } 278 | else { 279 | // Convert file to something usable 280 | if ($key === 'file') { 281 | $value = $this->addPostFile($value); 282 | } 283 | 284 | $return[$name] = $value; 285 | } 286 | } 287 | 288 | return $return; 289 | } 290 | 291 | /** 292 | * POST file upload 293 | * 294 | * @param string $filename File to be uploaded 295 | * 296 | * @return mixed 297 | * @throws InvalidArgumentException 298 | */ 299 | public function addPostFile($filename) 300 | { 301 | if ($filename instanceof UploadedFile) { 302 | 303 | // Get original filename 304 | $name = $filename->getClientOriginalName(); 305 | 306 | // Move the file 307 | $file = $filename->move(sys_get_temp_dir() . '/' . uniqid(), $name); 308 | 309 | // Get the new file path 310 | $filename = $file->getRealPath(); 311 | } 312 | 313 | if (!is_readable($filename)) { 314 | throw new InvalidArgumentException("Unable to open {$filename} for reading"); 315 | } 316 | 317 | // PHP 5.5 introduced a CurlFile object that deprecates the old @filename syntax 318 | // See: https://wiki.php.net/rfc/curl-file-upload 319 | if (function_exists('curl_file_create')) { 320 | return curl_file_create($filename); 321 | } 322 | 323 | // Use the old style if using an older version of PHP 324 | return "@{$filename}"; 325 | } 326 | 327 | /* 328 | * Check for server errors 329 | * 330 | * @param string $json 331 | * 332 | * @throws Exception 333 | */ 334 | private function checkForCurlErrors($json) 335 | { 336 | if ($json && isset($json->errors)) { 337 | // Get errors 338 | $errors = implode(', ', array_map(function ($error) { 339 | return $error->message; 340 | }, $json->errors)); 341 | 342 | // fetch sync key for event handling 343 | if (isset($json->sync)) { 344 | $this->syncKey = $json->sync; 345 | } 346 | 347 | throw new Exception($errors, $this->http_code); 348 | } 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /src/Facade/Asana.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom( 17 | __DIR__ . '/../config/asana.php', 'asana' 18 | ); 19 | } 20 | 21 | /** 22 | * Register the service provider. 23 | * 24 | * @return void 25 | */ 26 | public function register() 27 | { 28 | $this->registerAsanaService(); 29 | 30 | if ($this->app->runningInConsole()) { 31 | $this->registerResources(); 32 | } 33 | } 34 | 35 | /** 36 | * Register the Asana service. 37 | * 38 | * @return void 39 | */ 40 | public function registerAsanaService() 41 | { 42 | $this->app->singleton('torann.asana', function ($app) { 43 | $config = $app->config->get('asana', []); 44 | 45 | return new Asana($config); 46 | }); 47 | } 48 | 49 | /** 50 | * Register Asana resources. 51 | * 52 | * @return void 53 | */ 54 | public function registerResources() 55 | { 56 | if ($this->isLumen() === false) { 57 | $this->publishes([ 58 | __DIR__ . '/../config/asana.php' => config_path('asana.php'), 59 | ], 'config'); 60 | } 61 | } 62 | 63 | /** 64 | * Check if package is running under Lumen app 65 | * 66 | * @return bool 67 | */ 68 | protected function isLumen() 69 | { 70 | return Str::contains($this->app->version(), 'Lumen') === true; 71 | } 72 | 73 | /** 74 | * Get the services provided by the provider. 75 | * 76 | * @return array 77 | */ 78 | public function provides() 79 | { 80 | return []; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 |