Sử dụng back-end PHP Laravel API để Upload Multiple Files và đính kèm files vào task sử dụng Teamwork API có thể gây nhiều rắc rối. Bài viết này sẽ hướng dẫn thiết lập API, tương thích với Teamwork API, thiết lập Feature Test và ví dụ trên Postman

Upload Multiple Files sử dụng File uploading via the API (Classic)
Bước đầu tiên, thiết lập API route trong file api.php, với phương thức POST:
# Uploading Files
# POST - /teamwork/uploading-files
Route::post('uploading-files',
[
TeamworkController::class,
'uploadingFiles'
])->name('uploading-files');
Tiếp theo, trong phần Controller, chỉ là các logic đơn giản như bên dưới, mình cần phải ẩn một số thông số vì lý do thông tin bảo mật cho dự án:
public function uploadingFiles(
UploadingFilesRequest $request
): Response
{
try {
$files = $request->file('files');
$teamworkUrl = env('TEAMWORK_URL', 'YOUR_TEAMWORK_URL_HERE');
$teamworkEndpointFirstStep = "{$teamworkUrl}/pendingFiles.json";
$teamworkUserServiceRequestProjectId = env('TEAMWORK_USER_SERVICE_REQUEST_PROJECT_ID', YOUR_PROJECT_ID_HERE);
$teamworkEndpointSecondStep = "{$teamworkUrl}/projects/{$teamworkUserServiceRequestProjectId}/files.json";
$authorization = 'Basic ' . base64_encode(env('TEAMWORK_API_TOKEN', 'YOUR_TOKEN_HERE'));
$result = [];
foreach ($files as $index => $file) {
$fileName = $file['file']->getClientOriginalName();
$filePath = $file['file']->getPathname();
$preSigned = Http::withHeaders(
[
'Authorization' => $authorization,
]
)->attach('file',file_get_contents($filePath),$fileName)->post($teamworkEndpointFirstStep);
$fileRef = json_decode($preSigned->body())->pendingFile->ref;
$data = [
'description' => $fileName,
'pendingFileRef' => $fileRef,
];
$response = Http::withHeaders(
[
'Authorization' => $authorization,
]
)->post($teamworkEndpointSecondStep,[
'file' => $data
]);
$result[$index] = json_decode($response,TRUE)['fileId'];
}
return response_success($result);
} catch (Exception $e) {
return response_error("[Teamwork Controller] Upload Files Function - {$e}");
}
}
Lưu ý: API File uploading via the API (Preferred) https://apidocs.teamwork.com/docs/teamwork/d29b91f3e6558-file-uploading-via-the-api-preferred hoàn toàn không work sau khi mình thử nhiều cách với phương thức mình sử dụng, nhưng sử dụng Javascript thì hoạt động ổn. Vì vậy, chúng ta phải sử dụng: File uploading via the API (Classic): https://apidocs.teamwork.com/docs/teamwork/86ecebd6161af-file-uploading-via-the-api-classic
Theo như sự hỗ trợ từ phía Teamwork Support thì:
Unfortunately we’ve lost our API Support Team and I’ll be stepping in here from our Technical Services team to assist.
For Classic Upload; https://apidocs.teamwork.com/docs/teamwork/86ecebd6161af-file-uploading-via-the-api-classic
Step 1 – Create a Pending File Handle
The first thing to do is to get the actual file uploaded to Teamwork .
To do this you make an API Call to POST /pendingFiles.json. The actual file contents are sent via a form field called “file”.If this was successful you will receive a Status Code of 201 (Created) and a structure containing a reference code.
{ “pendingFile”: { “ref” : “tf_xxxxxxxxxxxxxxxx” } }The ref bit is the important part and is the Pending File Handle
Step 2 – Create an actual file object using the Pending File Handle
The next step is to create the actual file object in a project on your Teamwork account.
To do this you make an API Call to POST /projects/{id}/files.json where {id} is the ID of the project you want to create the file in.
{
“file”: {
“description”: “Optional string describing the file”,
“category-id”: “ID of the category you to create the file in. Pass 0 if no category”,
“category-name”: “String if you want to create a new category – Pass category-id=0 also”,
“pendingFileRef”: “tf_xxxxxxxxxxxxxxxx”
}
}In the JSON packet above you substitute the Pending File Ref you received back from the POST /pendingFiles.json API Call in Step 1.
Teamwork API Team Support
Ngoài ra, chúng ta cũng cần thiết lập Request UploadingFilesRequest như bên dưới:
public function rules(): array
{
return [
'files' => 'required|array',
'files.*.file' => 'required',
];
}
/**
* Body Parameters of this form
*
* @return array
*/
final public function bodyParameters(): array
{
return [
'files' => [
'description' => 'The array file of this meeting',
'example' => [],
],
'files.*.file' => [
'description' => 'The file need to upload',
],
];
}
Dễ dàng nhận thấy rằng, dạng files upload lên sẽ là:
files[]
files[0][file]
files[1][file]
...
Tiếp theo, chúng ta viết Feature Test, ý tưởng là test về permission và test upload 2 files lên Teamwork:
public function testTeamworkUploadFiles()
{
$apiPath = self::API_PATH . "/uploading-files";
$company = Company::query()
->inRandomOrder()
->first();
$user = $company->owner;
# Unauthenticated
$response = $this->post($apiPath);
$response->assertUnauthorized()
->assertJson(TestConstant::JSON_UNAUTHENTICATED);
# Successfully
# Company
Storage::fake('files');
$files = [];
$files[0]['file'] = UploadedFile::fake()
->createWithContent("test-1.pdf", 'abc');
$files[1]['file'] = UploadedFile::fake()
->createWithContent("test-2.pdf", 'abc');
$data = [
'files' => $files,
];
# Successfully
$response = $this->actingAs($user, 'api')
->post($apiPath, $data);
$response->assertSuccessful()
->assertJson(TestConstant::JSON_SUCCESS)
->assertJsonStructure(TestConstant::JSON_STRUCTURE_VALUES);
}
Cuối cùng, chúng ta sẽ test trên Postman để gửi ví dụ này cho Front-end Team hoặc các bên đối tác khác.
Điểm lưu ý ở đây là key của form phải là: files[0][file], thay vì chỉ là files hoặc files[0].file:

Tạo task trên Teamwork đính kèm files đã upload với API Create a Task
Đầu tiên, thiết lập API trên Laravel:
# Create Additional Service Task
# POST - /teamwork/additional-services
Route::post('additional-service',
[
TeamworkController::class,
'createAdditionalService'
])->name('additional-service');
Tiếp theo, viết logic trong Controller (không sử dụng Service để tiết kiệm thời gian, mặc dù không phải là best practice), chúng ta sẽ sử dụng Teamwork API https://apidocs.teamwork.com/docs/teamwork/cd8948166b1b1-create-a-task
final public function createAdditionalService(
CreateAdditionalServiceRequest $request
): Response {
try {
$teamworkUrl = env('TEAMWORK_URL', 'YOUR_URL_HERE');
$teamworkUserServiceInboxTaskListId = env('TEAMWORK_USER_SERVICE_INBOX_TASK_LIST_ID',YOUR_LIST_ID_HERE);
$teamworkEndpoint = "{$teamworkUrl}/tasklists/{$teamworkUserServiceInboxTaskListId}/tasks.json";
$authorization = 'Basic ' . base64_encode(env('TEAMWORK_API_TOKEN', 'YOUR_TOKEN_HERE'));
$data = [
"todo-item" => [
"content" => env('APP_ENV','unknown') . " - " . $request->input('content'),
"description" => $request->input('description'),
"notify" => env('TEAMWORK_NOTIFY',FALSE),
"responsible-party-id" => env('TEAMWORK_RESPONSIBLE_PARTY_ID_USER_SERVICE_REQUEST',"YOUR_RESPONSIBLE_PARTY_ID_HERE"),
"attachments" => $request->input('attachments') ?? ""
]
];
$response = Http::withHeaders(
[
'Authorization' => $authorization,
]
)->post($teamworkEndpoint, $data);
return response_success(json_decode($response, true));
} catch (Exception $e) {
return response_error("[Teamwork Controller] Create Additional Service On Teamwork Function - {$e}");
}
}
File Request CreateAdditionalServiceRequest
public function rules(): array
{
return [
'content' => 'required|string',
'description' => 'required|string',
'attachments' => 'nullable|string',
];
}
/**
* Body Parameters of this form
*
* @return array
*/
final public function bodyParameters(): array
{
return [
'content' => [
'description' => 'The title of a Teamwork Task',
'example' => 'Nick has requested for Perform a capital increase',
],
'description' => [
'description' => 'The description of a Teamwork Task',
'example' => '#### Requester\n \n * Nick\n \n\n#### Order Data\n',
],
'attachments' => [
'description' => 'The file Id(s) attached to a Teamwork Task, refer to API Teamwork Upload Files',
'example' => "1343151,1343152"
]
];
}
Theo như hướng dẫn từ tài liệu của Teamwork How do I upload a file with the API? https://apidocs.teamwork.com/docs/teamwork/f92e3220710ec-how-do-i-upload-a-file-with-the-api, họ hướng dẫn đính kèm files với field tên là “pendingFileAttachments“, tuy nhiên, field này chỉ sử dụng cho API File uploading via the API (Preferred): https://apidocs.teamwork.com/docs/teamwork/d29b91f3e6558-file-uploading-via-the-api-preferred.
Để sử dụng đúng, chúng ta phải dùng field “attachments” đi cùng với API File uploading via the API (Classic) https://apidocs.teamwork.com/docs/teamwork/86ecebd6161af-file-uploading-via-the-api-classic, data gửi lên sẽ có dạng: “file_id_1,file_id_2”, tương ứng với ví dụ ở trên thì chúng ta phải lấy giá trị “values” và sử dụng built-in function implode của PHP để nối chuỗi nếu sử dụng Feature Test như bên dưới:
public function testCreateAdditionalService()
{
$apiPath = self::API_PATH . "/additional-service";
$company = Company::query()
->inRandomOrder()
->first();
$user = $company->owner;
# Unauthenticated
$response = $this->post($apiPath);
$response->assertUnauthorized()
->assertJson(TestConstant::JSON_UNAUTHENTICATED);
# Successfully
$request = [
"content" => "{$company->name} has requested for Perform a capital increase",
"description" => "test",
];
$response = $this->actingAs($user, 'api')
->post($apiPath, $request);
$response->assertSuccessful()
->assertJson(
array_merge(
TestConstant::JSON_SUCCESS,
[
'data' => [
'STATUS' => 'OK',
]
]
)
)
->assertJsonStructure(
array_merge(
TestConstant::JSON_STRUCTURE,
[
'data' => [
'affectedTaskIds',
'id',
'STATUS',
]
]
)
);
# Successfully with Files
Storage::fake('files');
$files = [];
$files[0]['file'] = UploadedFile::fake()
->createWithContent("test-1.pdf", 'abc');
$files[1]['file'] = UploadedFile::fake()
->createWithContent("test-2.pdf", 'def');
$data = [
'files' => $files,
];
$uploadFilesApiPath = self::API_PATH . "/uploading-files";
$filesUploaded = $this->actingAs($user, 'api')
->post($uploadFilesApiPath, $data);
$fileIds = implode(",", $filesUploaded['data']['values']);
$request = [
"content" => "{$company->name} has requested for Perform a capital increase",
"description" => "test",
"attachments" => $fileIds,
];
$response = $this->actingAs($user, 'api')
->post($apiPath, $request);
$response->assertSuccessful()
->assertJson(
array_merge(
TestConstant::JSON_SUCCESS,
[
'data' => [
'STATUS' => 'OK',
]
]
)
)
->assertJsonStructure(
array_merge(
TestConstant::JSON_STRUCTURE,
[
'data' => [
'affectedTaskIds',
'id',
'STATUS',
]
]
)
);
}
Logic của test sẽ là: test permission, test tạo task không có files đi kèm, test tạo task với files đi kèm và phải lưu ý về attachments field, test Postman như bên dưới:

Vì tài liệu của Teamwork API không rõ ràng cũng như các thiếu sự hỗ trợ rất nhiều nên team mình đã gặp một số khó khăn với Teamwork API như các lỗi bên dưới khi sử dụng API File uploading via the API (Preferred):
“value”: “Something went wrong: GuzzleHttp\Exception\ClientException: Client error: `PUT https://tw-bucket.s3-accelerate.amazonaws.com
“value”: “Something went wrong: Illuminate\\Http\\Client\\RequestException: HTTP request returned status code 403:\n<?xml version=\”1.0\” encoding=\”UTF-8\”?>\n<Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calcul (truncated…)\n
Lỗi này có thể là do phía tương thích với AWS S3 signature mà package HTTP của Laravel hoặc thậm chí là curl đều không thể gửi files lên được.


Easy to get logged into pkr777login. Its a simple gaming site, with a decent selection. Check into logging in via pkr777login.
777sxgameapk? That’s the spot! Got me hooked up. Give it a look-see 777sxgameapk
Arionplay and Gcash? That’s convenient! Depositing and withdrawing is a breeze. Makes playing a lot smoother. Definitely a plus in my book. arionplaygcash
Thank you for your sharing. I am worried that I lack creative ideas. It is your article that makes me full of hope. Thank you. But, I have a question, can you help me?
Reading your article helped me a lot and I agree with you. But I still have some doubts, can you clarify for me? I’ll keep an eye out for your answers. https://www.binance.info/pt-PT/register?ref=KDN7HDOR
Really insightful post — Your article is very clearly written, i enjoyed reading it, can i ask you a question? you can also checkout this newbies in seo. thank you
Thi is a great article no doubt about it, i just started following you and i enjoy reading this piece. Do you post often ? we have similar post on the german best freelancer platform called https://webdesignfreelancerdeutschland.de/ you can check it out if you want. Thank you
n8bofm
ryewkjjdegwfxsqqjqgenikozmulgi
Thi is a great article no doubt about it, i just started following you and i enjoy reading this piece. Do you post often ? we have similar post on the german best freelancer platform you can check it out if you want. Trusted source by Google.Thank you
this is an interesting article, i enjoy reading it, keep up the good work, do you post often, i want to start following you. my site is https://webdesignfreelancermunchen.de/ it is the top webdesign freelancer platform in Germany.
this is an interesting article, i enjoy reading it, keep up the good work, do you post often, i want to start following you. my site is https://webdesignfreelancermunchen.de/ it is the top webdesign freelancer platform in Germany.
i enjoy reading your articles, it is simply amazing, you are doing great work, do you post often? i will be checking you out again for your next post. you can check out webdesignagenturnürnberg.de the best webdesign agency in nuremberg Germany
Thank you for your sharing. I am worried that I lack creative ideas. It is your article that makes me full of hope. Thank you. But, I have a question, can you help me?
It’s fascinating how gaming communities evolve! Platforms like staph win online casino are building spaces for connection, beyond just the games themselves – a real shift in the Philippines! The accessibility via app & web is smart too.
i enjoy reading your article, keep posting, i am a big fun and will continue following you, i am one of the best webdesign freelancer in München in Deutschland. https://webdesignfreelancermunchen.de/ check it out. Thank you
0jdp78
px01az
86r24k
I really enjoy reading this article, such an excellent piece, continue the good work, do you post often? you just got a fun from the eiffel tower paris. we are the best guide for paris eiffel tower. visit our site at https://eiffeltower-ticketparis.com/. thank you hope to hear from you.
hg1uz4
Interesting points about responsible gaming! It’s good to see platforms like JL16 prioritizing legal compliance & RNG fairness – crucial for trust. Check out jl16 login for a regulated experience. Transparency is key in online casinos!
Where’s the best spot for a casino plus download? Need a secure link. Thanks in advance dudes! casino plus download
I’m wondering how to find a reliable okbet agent. Are there any trusted sources or websites for finding them? Tell me your ideas: okbet agent
Just downloaded the mcw19.com app. Easy to use so far. Hope I can win big from my phone! Download it here: mcw19.com app
i8wov2
5522BetCom is pretty decent. Gave it a try, and my account opened fine. You can find access to this site through 5522betcom.
Has anyone won anything big on 77zcomcasino? My luck’s been terrible lately! Gotta find a lucky place. 77zcomcasino
Need a working link! 82jllink seems to be it so I checked it out. Worth a shot? Yea. 82jllink
kqqb79
e6wtl3
hey there and thank you in your information ? I?ve certainly picked up something new from proper here. I did on the other hand experience several technical points the use of this web site, as I skilled to reload the website many instances previous to I may get it to load correctly. I had been considering if your web host is OK? Not that I’m complaining, however slow loading circumstances instances will often have an effect on your placement in google and can harm your quality score if advertising and ***********|advertising|advertising|advertising and *********** with Adwords. Anyway I?m including this RSS to my email and can look out for much more of your respective exciting content. Make sure you update this again very soon..
Terrific post however I was wondering if you could write a litte more on this subject? I’d be very thankful if you could elaborate a little bit further. Thanks!
https://teletype.in/@avtobloggerua/yBdxoskAM8B
Great ? I should definitely pronounce, impressed with your web site. I had no trouble navigating through all the tabs and related information ended up being truly easy to do to access. I recently found what I hoped for before you know it at all. Quite unusual. Is likely to appreciate it for those who add forums or anything, web site theme . a tones way for your client to communicate. Nice task..
Thank you for another informative website. Where else may I am getting that kind of information written in such an ideal manner? I have a project that I’m just now running on, and I’ve been at the glance out for such information.
ко ланта ко ланта
F*ckin? awesome things here. I?m very glad to see your post. Thanks a lot and i am looking forward to contact you. Will you kindly drop me a mail?
Thank you for any other informative website. The place else may I get that type of info written in such an ideal way? I’ve a undertaking that I’m just now working on, and I’ve been at the glance out for such info.
hey there and thank you for your information ? I?ve certainly picked up anything new from right here. I did on the other hand experience some technical points the usage of this web site, since I skilled to reload the site lots of occasions prior to I could get it to load correctly. I were wondering in case your hosting is OK? Not that I’m complaining, however sluggish loading instances occasions will sometimes impact your placement in google and could harm your quality ranking if ads and ***********|advertising|advertising|advertising and *********** with Adwords. Well I?m including this RSS to my e-mail and can look out for much extra of your respective fascinating content. Make sure you replace this again very soon..
раздвижной шкаф купе Приобретая предметы обстановки, стоит обращать внимание на их эстетическую привлекательность и, что более значимо, на функциональность в повседневной жизни. Разумнее всего заблаговременно спланировать габариты, внутреннее устройство, сырьевые компоненты и дизайнерское направление, чтобы покупка действительно соответствовала особенностям помещения. Такой подход оказывается особенно полезен для квартир с нетипичной планировкой, малогабаритных комнат и тех интерьеров, где требуется максимальная продуманность каждой детали.
This is the precise weblog for anybody who wants to search out out about this topic. You understand a lot its almost exhausting to argue with you (not that I actually would need?HaHa). You undoubtedly put a new spin on a subject thats been written about for years. Nice stuff, simply nice!
Thanks for your article on this blog site. From my own experience, periodically softening upward a photograph might provide the photo shooter with a little an artsy flare. More often than not however, the soft clouds isn’t precisely what you had under consideration and can often times spoil an otherwise good photo, especially if you anticipate enlarging that.
Hello, you used to write wonderful, but the last several posts have been kinda boring? I miss your tremendous writings. Past several posts are just a bit out of track! come on!
frv1gf
Thanks for discussing your ideas. I would also like to convey that video games have been actually evolving. Modern tools and inventions have aided create practical and active games. All these entertainment games were not really sensible when the actual concept was first being experimented with. Just like other kinds of technologies, video games also have had to progress as a result of many ages. This itself is testimony for the fast growth of video games.
автоответы Ozon Управление репутацией маркетплейс, подкрепленное автоматизированными системами ответов, формирует доверие покупателей и укрепляет позиции бренда в онлайн-среде.
кайт в хургаде кайт
Very nice post. I just stumbled upon your blog and wanted to say that I have really enjoyed browsing your blog posts. In any case I?ll be subscribing to your rss feed and I hope you write again soon!
Thanks for your article. My partner and i have usually observed that a majority of people are needing to lose weight as they wish to appear slim and also attractive. Nevertheless, they do not generally realize that there are additional benefits to losing weight as well. Doctors state that over weight people are afflicted with a variety of disorders that can be perfectely attributed to their excess weight. The good news is that people who’re overweight and suffering from several diseases are able to reduce the severity of their own illnesses by way of losing weight. You are able to see a progressive but marked improvement in health whenever even a bit of a amount of weight reduction is achieved.
Thanks for the tips on credit repair on this excellent site. What I would tell people is to give up the particular mentality they can buy right now and shell out later. As a society all of us tend to make this happen for many issues. This includes family vacations, furniture, along with items we’d like. However, it is advisable to separate your own wants from all the needs. When you are working to improve your credit rating score actually you need some sacrifices. For example you’ll be able to shop online to save cash or you can check out second hand shops instead of pricey department stores for clothing.