Configuring Uppy to Use Multipart Uploads with Laravel/Vue - laravel

I figured it out
This was the missing piece. Once I clean up my code, I'll post an answer so that hopefully the next poor soul that has to deal with this will not have to go through the same hell I went through ;)
$command = $client->getCommand('UploadPart', array(
'Bucket' => 'the-bucket-name',
'Key' => $key,
'PartNumber' => $partNumber,
'UploadId' => $uploadId,
'Body' => '',
));
$signedUrl = $client->createPresignedRequest($command, '+20 minutes');
$presignedUrl = (string)$signedUrl->getUri();
return response()->json(['url' => $presignedUrl]);
I'm trying to figure out how to configure my server to work with Uppy for uploading multipart uploads to AWS S3 by using the CompanionUrl option. https://uppy.io/docs/aws-s3-multipart/#createMultipartUpload-file.
This is where I got the idea to go this route https://github.com/transloadit/uppy/issues/1189#issuecomment-445521442.
I can't figure this out and I feel like others have been stuck as well with no answer, so I'm posting what I've come up with so far in trying to get Uppy to work with multipart uploads using Laravel/Vue.
For the Vue component I have this:
<template>
<div>
<a id="uppy-trigger" #click="isUppyOpen = !isUppyOpen">Open Uppy</a>
<dashboard-modal
:uppy="uppy"
:open="isUppyOpen"
:props="{trigger: '#uppy-trigger'}"
/>
</div>
</template>
<script>
import Uppy from '#uppy/core'
import AwsS3Multipart from '#uppy/aws-s3-multipart';
import '#uppy/core/dist/style.css';
import '#uppy/dashboard/dist/style.css';
export default {
components: {
'dashboard-modal': DashboardModal,
},
data() {
return {
isUppyOpen: false,
}
},
computed: {
// Uppy Instance
uppy: () => new Uppy({
logger: Uppy.debugLogger
}).use(AwsS3Multipart, {
limit: 4,
companionUrl: 'https://mysite.local/',
}),
},
beforeDestroy () {
this.uppy.close();
},
}
</script>
Then for the routing I've added this to my web.php file.
// AWS S3 Multipart Upload Routes
Route::name('s3.multipart.')->prefix('s3/multipart')
->group(function () {
Route::post('/', ['as' => 'createMultipartUpload', 'uses' => 'AwsS3MultipartController#createMultipartUpload']);
Route::get('{uploadId}', ['as' => 'getUploadedParts', 'uses' => 'AwsS3MultipartController#getUploadedParts']);
Route::get('{uploadId}/{partNumber}', ['as' => 'signPartUpload', 'uses' => 'AwsS3MultipartController#signPartUpload']);
Route::post('{uploadId}/complete', ['as' => 'completeMultipartUpload', 'uses' => 'AwsS3MultipartController#completeMultipartUpload']);
Route::delete('{uploadId}', ['as' => 'abortMultipartUpload', 'uses' => 'AwsS3MultipartController#abortMultipartUpload']);
});
Basically what is happening is that I've set the "companionUrl" to "https://mysite.local/", then Uppy will send five requests when uploading a multipart upload file to these routes, ie "https://mysite.local/s3/multipart/createMultipartUpload".
I then created a controller to handle the requests:
<?php
namespace App\Http\Controllers;
use Aws\S3\S3Client;
use Illuminate\Http\Request;
class AwsS3MultipartController extends Controller
{
public function createMultipartUpload(Request $request)
{
$client = new S3Client([
'version' => 'latest',
'region' => 'us-east-1',
]);
$key = $request->has('filename') ? $request->get('filename') : null;
$type = $request->has('type') ? $request->get('type') : null;
if (!is_string($key)) {
return response()->json(['error' => 's3: filename returned from "getKey" must be a string'], 500);
}
if (!is_string($type)) {
return response()->json(['error' => 's3: content type must be a string'], 400);
}
$response = $client->createMultipartUpload([
'Bucket' => 'the-bucket-name',
'Key' => $key,
'ContentType' => $type,
'Expires' => 60
]);
$mpuKey = !empty($response['Key']) ? $response['Key'] : null;
$mpuUploadId = !empty($response['UploadId']) ? $response['UploadId'] : null;
if (!$mpuKey || !$mpuUploadId) {
return response()->json(['error' => 'Unable to process upload request.'], 400);
}
return response()->json([
'key' => $mpuKey,
'uploadId' => $mpuUploadId
]);
}
public function getUploadedParts($uploadId)
{
// Haven't configured this route yet as I haven't made it this far.
return $uploadId;
}
public function signPartUpload(Request $request, $uploadId, $partNumber)
{
$client = new S3Client([
'version' => 'latest',
'region' => 'us-east-1',
]);
$key = $request->has('key') ? $request->get('key') : null;
if (!is_string($key)) {
return response()->json(['error' => 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"'], 400);
}
if (!intval($partNumber)) {
return response()->json(['error' => 's3: the part number must be a number between 1 and 10000.'], 400);
}
// Creating a presigned URL. I don't think this is correct.
$cmd = $client->getCommand('PutObject', [
'Bucket' => 'the-bucket-name',
'Key' => $key,
'UploadId' => $uploadId,
'PartNumber' => $partNumber,
]);
$response = $client->createPresignedRequest($cmd, '+20 minutes');
$presignedUrl = (string)$response->getUri();
return response()->json(['url' => $presignedUrl]);
}
public function completeMultipartUpload(Request $request, $uploadId)
{
$client = new S3Client([
'version' => 'latest',
'region' => 'us-east-1',
]);
$key = $request->has('key') ? $request->get('key') : null;
$parts = json_decode($request->getContent(), true)['parts'];
if (!is_string($key)) {
return response()->json(['error' => 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"'], 400);
}
if (!is_array($parts) || !$this->arePartsValid($parts)) {
return response()->json(['error' => 's3: "parts" must be an array of {ETag, PartNumber} objects.'], 400);
}
// The completeMultipartUpload method fails with the following error.
// "Error executing "CompleteMultipartUpload" on "https://the-bucket-name.s3.amazonaws.com/NewProject.png?uploadId=nlWLdbNgB9zgarpLBXnj17eOIGAmQM_xyBArymtwdM71fhbFvveggDmL6fz4blz.B95TLhMatDvodbMb5p2ZMKqdlLeLFoSW1qcu33aRQTlt6NbiP_dkDO90DFO.pWGH"; AWS HTTP error: Client error: `POST https://the-bucket-name.s3.amazonaws.com/NewProject.png?uploadId=nlWLdbNgB9zgarpLBXnj17eOIGAmQM_xyBArymtwdM71fhbFvveggDmL6fz4blz.B95TLhMatDvodbMb5p2ZMKqdlLeLFoSW1qcu33aRQTlt6NbiP_dkDO90DFO.pWGH` resulted in a `400 Bad Request` response:
// <Error><Code>InvalidPart</Code><Message>One or more of the specified parts could not be found. The part may not have be (truncated...)
// InvalidPart (client): One or more of the specified parts could not be found. The part may not have been uploaded, or the specified entity tag may not match the part's entity tag. - <Error><Code>InvalidPart</Code><Message>One or more of the specified parts could not be found. The part may not have been uploaded, or the specified entity tag may not match the part's en"
$result = $client->completeMultipartUpload([
'Bucket' => 'the-bucket-name',
'Key' => $key,
'UploadId' => $uploadId,
'MultipartUpload' => [
'Parts' => $parts,
],
]);
return response()->json(['location' => $result['location']]);
}
public function abortMultipartUpload($uploadId)
{
// Haven't configured this route yet as I haven't made it this far.
return $uploadId;
}
private function arePartsValid($parts)
{
// Validation for the parts will go here, but returning true for now.
return true;
}
}
I can upload a multipart file fine purely PHP/server-side. For huge files though, this isn't going to work though since I would have to wait for the upload to finish on my server, then upload it to AWS in the parts.
$s3_client = new S3Client([
'version' => 'latest',
'region' => 'us-east-1',
]);
$bucket = 'the-bucket-name';
$tmp_name = $request->file('file')->getPathname();
$folder = Carbon::now()->format('Y/m/d/');
$filename = pathinfo($request->file('file')->getClientOriginalName(), PATHINFO_FILENAME);
$extension = $extension = pathinfo($request->file('file')->getClientOriginalName(), PATHINFO_EXTENSION);
$timestamp = Carbon::now()->format('H-i-s');
$name = "{$folder}{$filename}_{$timestamp}.{$extension}";
$response = $s3_client->createMultipartUpload([
'Bucket' => $bucket,
'Key' => $name,
]);
$uploadId = $response['UploadId'];
$file = fopen($tmp_name, 'r');
$parts = [];
$partNumber = 1;
while (! feof($file)) {
$result = $s3_client->uploadPart([
'Bucket' => $bucket,
'Key' => $name,
'UploadId' => $uploadId,
'PartNumber' => $partNumber,
'Body' => fread($file, 5 * 1024 * 1024),
]);
$parts[] = [
'PartNumber' => $partNumber++,
'ETag' => $result['ETag'],
];
}
$result = $s3_client->completeMultipartUpload([
'Bucket' => $bucket,
'Key' => $name,
'UploadId' => $uploadId,
'MultipartUpload' => [
'Parts' => $parts,
],
]);
What I believe is happening is that Uppy is handling the while loop part client-side. In order to do that, I have to return a pre-signed URL Uppy can use, but the pre-signed URL I'm currently returning isn't correct.
One thing I noted is that when I step through the while loop when initiating the multipart upload purely server-side, no file is uploaded to my bucket until the completeMultipartUpload method is fired. If however, I step through the parts being uploaded via Uppy, the parts seem to be being uploaded as the final file and each part is just overwriting the previous part. I'm then left with a fragment of the file, ie the last 3.5MB of a 43.5MB file.

Here's how I was able to get Uppy, Vue, and Laravel to play nicely together.
The Vue Component:
<template>
<div>
<a id="uppy-trigger" #click="isUppyOpen = !isUppyOpen">Open Uppy</a>
<dashboard-modal
:uppy="uppy"
:open="isUppyOpen"
:props="{trigger: '#uppy-trigger'}"
/>
</div>
</template>
<script>
import Uppy from '#uppy/core'
import AwsS3Multipart from '#uppy/aws-s3-multipart';
import '#uppy/core/dist/style.css';
import '#uppy/dashboard/dist/style.css';
export default {
components: {
'dashboard-modal': DashboardModal,
},
data() {
return {
isUppyOpen: false,
}
},
computed: {
// Uppy Instance
uppy: () => new Uppy({
logger: Uppy.debugLogger
}).use(AwsS3Multipart, {
limit: 4,
companionUrl: 'https://mysite.local/',
}),
},
beforeDestroy () {
this.uppy.close();
},
}
</script>
The Routing:
// AWS S3 Multipart Upload Routes
Route::name('s3.multipart.')->prefix('s3/multipart')
->group(function () {
Route::post('/', ['as' => 'createMultipartUpload', 'uses' => 'AwsS3MultipartController#createMultipartUpload']);
Route::get('{uploadId}', ['as' => 'getUploadedParts', 'uses' => 'AwsS3MultipartController#getUploadedParts']);
Route::get('{uploadId}/{partNumber}', ['as' => 'signPartUpload', 'uses' => 'AwsS3MultipartController#signPartUpload']);
Route::post('{uploadId}/complete', ['as' => 'completeMultipartUpload', 'uses' => 'AwsS3MultipartController#completeMultipartUpload']);
Route::delete('{uploadId}', ['as' => 'abortMultipartUpload', 'uses' => 'AwsS3MultipartController#abortMultipartUpload']);
});
The Controller:
<?php
namespace App\Http\Controllers;
use Aws\S3\S3Client;
use Carbon\Carbon;
use Exception;
use Illuminate\Http\Request;
class AwsS3MultipartController extends Controller
{
private $bucket;
private $client;
public function __construct()
{
$this->bucket = 'the-name-of-the-bucket';
$this->client = new S3Client([
'version' => 'latest',
'region' => 'us-east-1',
]);
}
/**
* Create/initiate the multipart upload
* #param Request $request
* #return JsonResponse
*/
public function createMultipartUpload(Request $request)
{
// Get the filename and type from request
$filename = $request->has('filename') ? $request->get('filename') : null;
$type = $request->has('type') ? $request->get('type') : null;
// Check filename
if (!is_string($filename)) {
return response()->json(['error' => 's3: filename returned from "getKey" must be a string'], 500);
}
// Check type
if (!is_string($type)) {
return response()->json(['error' => 's3: content type must be a string'], 400);
}
// Set up key equal to YYYY/MM/DD/filename_H-i-s.ext
$fileBaseName = pathinfo($filename, PATHINFO_FILENAME);
$extension = pathinfo($filename, PATHINFO_EXTENSION);
$folder = Carbon::now()->format('Y/m/d/');
$timestamp = Carbon::now()->format('H-i-s');
$key = "{$folder}{$fileBaseName}_{$timestamp}.{$extension}";
// Create/initiate the multipart upload
try {
$response = $this->client->createMultipartUpload([
'Bucket' => $this->bucket,
'Key' => $key,
'ContentType' => $type,
'Expires' => 60
]);
} catch (Exception $e) {
return response()->json(['error' => $e->getMessage()], 400);
}
// Multipart upload key and id
$mpuKey = !empty($response['Key']) ? $response['Key'] : null;
$mpuUploadId = !empty($response['UploadId']) ? $response['UploadId'] : null;
// Check multipart upload key and id
if (!$mpuKey || !$mpuUploadId) {
return response()->json(['error' => 'Unable to process upload request.'], 400);
}
return response()->json([
'key' => $mpuKey,
'uploadId' => $mpuUploadId
]);
}
/**
* Get parts that have been uploaded
* #param Request $request
* #param string $uploadId
* #return JsonResponse
*/
public function getUploadedParts(Request $request, string $uploadId)
{
$key = $request->has('key') ? $request->get('key') : null;
// Check key
if (!is_string($key)) {
return response()->json(['error' => 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"'], 400);
}
$parts = [];
$getParts = true;
$startAt = 0;
// Get parts uploaded so far
while ($getParts) {
$partsPage = $this->listPartsPage($key, $uploadId, $startAt, $parts);
if (isset($partsPage['error'])) {
return response()->json(['error' => $partsPage['error']], 400);
}
if ($partsPage['isTruncated']) {
$startAt = $partsPage['nextPartNumberMarker'];
} else {
$getParts = false;
}
}
return response()->json(
$parts,
);
}
/**
* Create a pre-signed URL for parts to be uploaded to
* #param Request $request
* #param string $uploadId
* #param int $partNumber
* #return JsonResponse
*/
public function signPartUpload(Request $request, string $uploadId, int $partNumber)
{
$key = $request->has('key') ? $request->get('key') : null;
// Check key
if (!is_string($key)) {
return response()->json(['error' => 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"'], 400);
}
// Check part number
if (!intval($partNumber)) {
return response()->json(['error' => 's3: the part number must be a number between 1 and 10000.'], 400);
}
// Create the upload part command and get the pre-signed URL
try {
$command = $this->client->getCommand('UploadPart', [
'Bucket' => $this->bucket,
'Key' => $key,
'PartNumber' => $partNumber,
'UploadId' => $uploadId,
'Body' => '',
]);
$presignedUrl = $this->client->createPresignedRequest($command, '+20 minutes');
} catch (Exception $e) {
return response()->json(['error' => $e->getMessage()], 400);
}
// Convert the pre-signed URL to a string
$presignedUrlString = (string)$presignedUrl->getUri();
return response()->json(['url' => $presignedUrlString]);
}
/**
* Complete the multipart upload
* #param Request $request
* #param string $uploadId
* #return JsonResponse
*/
public function completeMultipartUpload(Request $request, string $uploadId)
{
$key = $request->has('key') ? $request->get('key') : null;
$parts = json_decode($request->getContent(), true)['parts'];
// Check the key
if (!is_string($key)) {
return response()->json(['error' => 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"'], 400);
}
// Check the parts
if (!is_array($parts) || !$this->arePartsValid($parts)) {
return response()->json(['error' => 's3: "parts" must be an array of {ETag, PartNumber} objects.'], 400);
}
// Complete the multipart upload
try {
$result = $this->client->completeMultipartUpload([
'Bucket' => $this->bucket,
'Key' => $key,
'UploadId' => $uploadId,
'MultipartUpload' => [
'Parts' => $parts,
],
]);
} catch (Exception $e) {
return response()->json(['error' => $e->getMessage()], 400);
}
// Change forwardslash entities to forwardslashes
$location = urldecode($result['Location']);
return response()->json(['location' => $location]);
}
public function abortMultipartUpload(Request $request, $uploadId)
{
$key = $request->has('key') ? $request->get('key') : null;
// Check the key
if (!is_string($key)) {
return response()->json(['error' => 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"'], 400);
}
// Cancel the multipart upload
try {
$response = $this->client->abortMultipartUpload([
'Bucket' => $this->bucket,
'Key' => $key,
'UploadId' => $uploadId,
]);
} catch (Exception $e) {
//
}
return response()->json();
}
private function listPartsPage(string $key, string $uploadId, int $startAt, array &$parts)
{
// Configure response
$response = [
'isTruncated' => false,
];
// Get list of parts uploaded
try {
$result = $this->client->listParts([
'Bucket' => $this->bucket,
'Key' => $key,
'PartNumberMarker' => $startAt,
'UploadId' => $uploadId,
]);
} catch (Exception $e) {
return ['error' => 's3: unable to continue upload. The upload may have been aborted.'];
}
// Add found parts to parts array
if ($result->hasKey('Parts')) {
array_push($parts, ...$result->get('Parts'));
}
// Check if parts are truncated
if ($result->hasKey('IsTruncated') && $result->get('IsTruncated')) {
$response['isTruncated'] = true;
$response['nextPartNumberMarker'] = $result->get('NextPartNumberMarker');
}
return $response;
}
/**
* Validate the parts for the multipart upload
* #param array $parts An associative array of parts with PartNumber and ETag
* #return bool
*/
private function arePartsValid(array $parts)
{
if (!is_array($parts)) {
return false;
}
foreach ($parts as $part) {
if (!is_int($part['PartNumber']) || !is_string($part['ETag'])) {
return false;
}
}
return true;
}
}

You can use this pre-built laravel package to easily achieve multipart uploading via laravel and uppy:
https://github.com/TappNetwork/laravel-uppy-s3-multipart-upload

Related

Laravel Many to one in Resource

I use laravel 8 & have 3 table:
Products, ProductPrice & ProductsPublisher:
this is my Products model for this relationship:
public function lastPrice(){
return $this->hasMany(ProductPrice::class)->where('status','active')->orderBy('created_at','DESC')->distinct('publisher_id');
}
and this is my productsPrice model for publisher relationship:
public function getPublisher(){
return $this->belongsTo(ProductsPublisher::class,'publisher_id');
}
now, i want to use laravel resource for my api, i wrote products resource:
public function toArray($request)
{
return [
'id' => $this->id,
'price' => lastPrice::make($this->lastPrice),
'status' => $this->status,
'slug' => $this->slug,
'title' => $this->title,
'description' => $this->description,
'txt' => $this->txt,
'lang' => $this->lang,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
but in lastPrice resource, when i wrote like this:
return [
'id' => $this->id,
'main_price' => $this->main_price
];
it give me this error:
Property [id] does not exist on this collection instance.
when i use this code:
return parent::toArray($request);
get response but because i need to use another relationship in my lastPirce for publishers, i cant use that code and should return separately my data.
What i should to do?
thanks
Edit 1:
this is my Controller Code:
$products = Product::where('id',$id)->where('slug',$slug)->where('status','confirm')->first();
if(!$products){
return $this->sendError('Post does not exist.');
}else{
return $this->sendResponse(new \App\Http\Resources\Products\Products($products), 'Posts fetched.');
}
and this is sendResponse & sendError:
public function sendResponse($result, $message)
{
$response = [
'success' => true,
'data' => $result,
'message' => $message,
];
return response()->json($response, 200);
}
public function sendError($error, $errorMessages = [], $code = 404)
{
$response = [
'success' => false,
'message' => $error,
];
if(!empty($errorMessages)){
$response['data'] = $errorMessages;
}
return response()->json($response, $code);
}
thanks.
Edit 2:
i change my lastPrice Resource toArray function to this and my problem solved, but i think this isn't a clean way, any better idea?
$old_data = parent::toArray($request);
$co = 0;
$new_data = [];
foreach ($old_data as $index){
$publisher_data = Cache::remember('publisher'.$index['publisher_id'], env('CACHE_TIME_LONG') , function () use ($index) {
return ProductsPublisher::where('id' , $index['publisher_id'])->first();
});
$new_data[$co]['main_prices'] = $index['main_price'];
$new_data[$co]['off_prices'] = $index['off_price'];
$new_data[$co]['publisher'] = SinglePublisher::make($publisher_data);
$new_data[$co]['created_at'] = $index['created_at'];
$co++;
}
return $new_data;

Laravel - How to perform Advance Excel Import Validation Message using Maatwebsite

I am using Laravel-8 and Maatwebsite-3.1 package to import Excel into the DB using Laravel API as the endpoint.
Trait:
trait ApiResponse {
public
function coreResponse($message, $data = null, $statusCode, $isSuccess = true) {
if (!$message) return response() - > json(['message' => 'Message is required'], 500);
// Send the response
if ($isSuccess) {
return response() - > json([
'message' => $message,
'error' => false,
'code' => $statusCode,
'results' => $data
], $statusCode);
} else {
return response() - > json([
'message' => $message,
'error' => true,
'code' => $statusCode,
], $statusCode);
}
}
public
function success($message, $data, $statusCode = 200) {
return $this - > coreResponse($message, $data, $statusCode);
}
public
function error($message, $statusCode = 500) {
return $this - > coreResponse($message, null, $statusCode, false);
}
}
Import:
class EmployeeImport extends DefaultValueBinder implements OnEachRow, WithStartRow, SkipsOnError, WithValidation, SkipsOnFailure
{
use Importable, SkipsErrors, SkipsFailures;
public function onRow(Row $row)
{
$rowIndex = $row->getIndex();
if($rowIndex >= 1000)
return; // Not more than 1000 rows at a time
$row = $row->toArray();
$employee = Employee::create([
'first_name' => $row[0],
'other_name' => $row[1] ?? '',
'last_name' => $row[2],
'email' => preg_replace('/\s+/', '', strtolower($row[3])),
'created_at' => date("Y-m-d H:i:s"),
'created_by' => Auth::user()->id,
]);
public function startRow(): int
{
return 2;
}
}
Controller:
public function importEmployee(Request $request)
{
try {
$validator = Validator::make($request->all(), [
'document' => 'file|mimes:xls,xlsx|max:5000',
]);
if ($request->hasFile('document'))
{
if($validator->passes()) {
$import = new EmployeeImport;
$file = $request->file('document');
$file->move(public_path('storage/file_imports/employee_imports'), $file->getClientOriginalName());
Excel::import($import, public_path('storage/file_imports/employee_imports/' . $file->getClientOriginalName() ));
foreach ($import->failures() as $failure) {
$importerror = new ImportError();
$importerror->data_row = $failure->row(); // row that went wrong
$importerror->data_attribute = $failure->attribute(); // either heading key (if using heading row concern) or column index
$importerror->data_errors = $failure->errors()[0]; // Actual error messages from Laravel validator
$importerror->data_values = json_encode($failure->values());
$importerror->created_by = Auth::user()->id;
$importerror->created_at = date("Y-m-d H:i:s");
$importerror->save();
}
return $this->success('Employees Successfully Imported.', [
'file' => $file
]);
}else{
return $this->error($validator->errors(), 422);
}
}
} catch(\Throwable $e) {
Log::error($e);
return $this->error($e->getMessage(), $e->getCode());
}
}
I made it to SkipOnError and SkipOnFailure.
If there's error, it saves the error into the DB. This is working.
However, there is issue, if some rows fail it still display success (Employees Successfully Imported) based on this:
return $this->success('Employees Successfully Imported.
When there is partial upload, or all the rows or some of the rows have issues, I want to display this to the user. So that it will be interactive.
How do I achieve this?
Thanks

Error column not found, but I did not declare the column?

I'm inserting a record to a polymorphic imageable table, however it says column thread_id not found. I have not declared this thread_id column and I don't know where it's pulling it from. Here is the code it's trying to run.
protected static function bootRecordImage()
{
if (auth()->guest()) return;
foreach (static::getMethodToRecord() as $event) {
static::$event(function ($model) use ($event) {
$body = request()->body;
preg_match_all('/<img .*?(?=src)src=\"([^\"]+)\"/si', $body, $matches);
$images = $matches[1];
if($event == 'created') {
foreach ($images as $image) {
$model->images()->create([
'user_id' => auth()->id(),
'imageable_id' => $model->id,
'imageable_type' => get_class($model),
'path' => $image
]);
}
}
if($event == 'deleting') {
foreach ($images as $image) {
$model->images()->delete([
'user_id' => auth()->id(),
'imageable_id' => $model->id,
'imageable_type' => get_class($model),
'path' => $image
]);
if (File::exists(public_path($image))) {
File::delete(public_path($image));
}
}
}
});
}
}
My store method:
public function store(Request $request, Channel $channel, Spam $spam)
{
if (!auth()->user()) {
return back()->withInput()->with('flash', 'Sorry! You must be logged in to perform this action.');
}
if (!auth()->user()->confirmed) {
return back()->withInput()->with('flash', 'Sorry! You must first confirm your email address.');
}
$this->validate($request, [
'title' => 'required',
'body' => 'required',
'channel_id' => 'required|exists:channels,id',
'g-recaptcha-response' => 'required'
// yes it's required, but it also needs to exist on the channels model, specifically on the id
]);
$response = Zttp::asFormParams()->post('https://www.google.com/recaptcha/api/siteverify', [
'secret' => config('services.recaptcha.secret'),
'response' => $request->input('g-recaptcha-response'),
'remoteip' => $_SERVER['REMOTE_ADDR']
]);
// dd($response->json());
if (! $response->json()['success']) {
throw new \Exception('Recaptcha failed');
}
$spam->detect(request('title'));
$spam->detect(request('body'));
$thread = Thread::create([
'user_id' => auth()->id(),
'channel_id' => request('channel_id'),
'title' => request('title'),
'body' => request('body'),
//'slug' => str_slug(request('title'))
]);
return redirect('/forums/' . $thread->channel->slug . '/' . $thread->slug);
}
As you can see, no where is a thread_id mentioned, yet in the error it looks like it's trying to insert into a thread_id column that I've never declared.
Thanks for reading.
I put the polymorphic relation in the model and the trait. Remove it from the Model and you're good to go.

Laravel broadcasting a Presence Channel using pusher unable to pass parameters

I've followed the documentation at https://laravel.com/docs/6.x/broadcasting step by step and make sure I copy and paste to be certain I don't make any mistake. I'm able to broadcast just fine and everything is working just fine. Because I'm unable to pass attributes, people in different roomId are counted as if they are all in the same room.
Here is the live example:
https://prayershub.com/worship/82 - 82 is the worship_id I would like to pass to:
Broadcast::channel('worship_presence_channel.{id}', function ($id) {
if(Auth()->check())
{
$profile = Auth()->user()->Profile;
$user = Auth()->user();
$data = [
'id' => $user->id,
'name' => $user->name,
'username' => $user->username,
'avatar' => config('app.storage').$profile->profile_image,
'url' => $profile->profile_url,
'favorite_bible_verse' => $profile->favorite_bible_verse
];
return $id;
}
});
From:
Echo.join(`worship_presence_channel.${id}`)
.here((users) => {
worshipers=users;
joinUser(worshipers);
$('.group-count').html(worshipers.length);
console.log(users);
})
.joining((user) => {
worshipers.push(user);
popupNewUser(user);
joinUser(worshipers);
$('.group-count').html(worshipers.length);
})
.leaving((user) => {
worshipers = worshipers.filter(function(obj) {
return (obj.id !== user.id);
});
popupLeaveUser(user);
joinUser(worshipers);
$('.group-count').html(worshipers.length);
});
I also have an event which seems to be unneccassary but it lools like this:
public function broadcastOn()
{
return new PresenceChannel('worship_presence_channel.58');
}
public function broadcastAs()
{
return 'worship_presence_channel.58';
}
Can anyone please, tell me what i'm doing wrong or if I get the whole thing just wrong. Please help!
I've figured it out, I've change the echo codes above to this:
Broadcast::channel('worship_presence_channel.{id}', function ($user, $id) {
if(Auth()->check())
{
$profile = $user->Profile;
$data = [
'id' => $user->id,
'name' => $user->name,
'username' => $user->username,
'avatar' => config('app.storage').$profile->profile_image,
'url' => $profile->profile_url,
'favorite_bible_verse' => $profile->favorite_bible_verse,
'worships_id' => $id
];
return $data;
}
});
I'm passing 2 parameters $user, $id and it works just as the doc said it would!!!

Laravel - Route can't be found when I added in a request validator

I have a function to handle payments in my app. When I change Request $request to PropertyPaymentRequest $request Is there any reason why?
public function ProcessAdvertPayment(Property $property, PropertyPaymentRequest $request)
{
$payload = $request->input('payload', false);
$nonce = $payload['nonce'];
$payment = Braintree_Transaction::sale([
'amount' => 1,
'paymentMethodNonce' => $nonce,
'creditCard' => [
'number' => request('cardnumber'), 'expirationDate' => request('month').'/'.request('year'),
"cvv" => request('cvv')
],
]);
if ($payment->success) {
$property->payment()->create($request);
}
return response()->json($payment);
}
Import of the request
use App\Http\Requests\PropertyPaymentRequest;
Route
Route::prefix('advertisement')->group(function () {
Route::post('{property}/payment', 'API\PropertyController#ProcessAdvertPayment');
});

Resources