Sentiment Analysis in Laravel: Step-by-Step Tutorial with a REST API

Sentiment Analysis in Laravel: Step-by-Step Tutorial with a REST API

Quick Answer

To add sentiment analysis to Laravel, store your API key in config/services.php, call POST https://rsflowhub.com/api/v1/ai/sentiment-analysis with the Http facade, and read data.result.sentiment and confidence from the JSON response. Run it inside a queued job so page requests never wait on the AI call.

Customer reviews, support messages and survey answers are full of signals, but nobody has time to read them all. In this tutorial you will add sentiment analysis to a Laravel application with a single REST call: no Python service, no model hosting and no prompt writing. You will get a sentiment label, a confidence score and optional per-aspect sentiment (for example delivery versus support), then store it on your own models.

What sentiment analysis gives your Laravel app

A star rating tells you how someone felt in one number. The text of the review tells you why, and sentiment analysis turns that text into something you can filter, count and alert on. Typical uses in a Laravel project include:

  • Support triage: flag negative tickets and messages so an agent sees angry customers first.
  • Review dashboards: chart the share of positive, neutral, negative and mixed reviews per product or per week.
  • Early warnings: notify the team when negative sentiment spikes after a release.
  • Product feedback: use per-aspect sentiment to learn that customers love your delivery but dislike pricing, even inside a single review.

The four labels returned by the API behave differently in practice, so decide in advance what each one should do:

Sentiment Example Typical action
positive"Arrived early, great quality."Candidate for testimonials and reviews on product pages
negative"Broke after two days. Useless."Alert support; follow up the same day
neutral"Package received on Tuesday."No action; exclude from satisfaction charts
mixed"Fast delivery, but support was slow."Read the aspect results to see which part went wrong

1. Store the API key

Create a free RSFlowHub account and copy your API key from the dashboard. Add it to .env and register it in config/services.php so it is cached with the rest of your config:

config/services.php PHP
// .env
// RSFLOWHUB_KEY=rfh_your_api_key

// config/services.php
'rsflowhub' => [
    'key' => env('RSFLOWHUB_KEY'),
    'url' => 'https://rsflowhub.com/api/v1/ai',
],

2. Make your first sentiment call

Laravel ships with the Http client, so there is nothing to install. Send the text, and optionally a list of aspects you care about:

routes/console.php PHP
use Illuminate\Support\Facades\Http;

$response = Http::withHeaders(['X-API-Key' => config('services.rsflowhub.key')])
    ->timeout(30)
    ->post(config('services.rsflowhub.url') . '/sentiment-analysis', [
        'text' => 'Delivery was fast, but support took three days to reply.',
        'aspects' => ['delivery', 'support'],
    ]);

$result = $response->json('data.result');
// $result['sentiment']         => 'mixed'
// $result['confidence']        => 0.91
// $result['aspect_sentiments'] => [['aspect' => 'delivery', 'sentiment' => 'positive', ...], ...]

Every RSFlowHub endpoint returns the same envelope: success, request_id, data.result and meta. The sentiment is one of positive, negative, neutral or mixed, and confidence is between 0 and 1.

3. Wrap it in a service class

Keep controllers thin by putting the API call in a small service. Returning null on failure lets callers decide what to do without try/catch everywhere:

app/Services/SentimentService.php PHP
<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class SentimentService
{
    /**
     * @return array{sentiment: string, confidence: float, aspect_sentiments?: array}|null
     */
    public function analyze(string $text, array $aspects = []): ?array
    {
        $response = Http::withHeaders(['X-API-Key' => config('services.rsflowhub.key')])
            ->timeout(30)
            ->retry(2, 1000, throw: false)
            ->post(config('services.rsflowhub.url') . '/sentiment-analysis', array_filter([
                'text' => mb_substr($text, 0, 5000),
                'aspects' => $aspects ?: null,
            ]));

        if ($response->failed()) {
            Log::warning('Sentiment analysis failed', [
                'status' => $response->status(),
                'code' => $response->json('error.code'),
                'request_id' => $response->json('request_id'),
            ]);

            return null;
        }

        return $response->json('data.result');
    }
}

The text field accepts up to 5,000 characters per request, so the service trims longer input. If you need to score a long document, analyse it paragraph by paragraph instead.

4. Analyse new reviews in a queued job

An AI call usually takes 1 to 3 seconds. That is fine in a background job, but too slow to block a checkout or form submission. Add two columns to your reviews table and fill them from a job:

database/migrations/add_sentiment_to_reviews.php PHP
Schema::table('reviews', function (Blueprint $table) {
    $table->string('sentiment', 16)->nullable()->index();
    $table->decimal('sentiment_confidence', 4, 3)->nullable();
});
app/Jobs/AnalyzeReviewSentiment.php PHP
<?php

namespace App\Jobs;

use App\Models\Review;
use App\Services\SentimentService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;

class AnalyzeReviewSentiment implements ShouldQueue
{
    use Queueable;

    public int $tries = 3;

    public function __construct(public Review $review) {}

    public function handle(SentimentService $sentiment): void
    {
        $result = $sentiment->analyze($this->review->body, ['quality', 'delivery', 'price']);

        if ($result === null) {
            $this->release(60); // try again in a minute

            return;
        }

        $this->review->update([
            'sentiment' => $result['sentiment'],
            'sentiment_confidence' => $result['confidence'],
        ]);
    }
}

Dispatch it whenever a review is created, for example from a model observer or right after saving: AnalyzeReviewSentiment::dispatch($review);. Now you can build a dashboard with Review::where('sentiment', 'negative') and alert your team about unhappy customers the same day.

5. Store aspect-level sentiment

The overall label is useful, but the aspect results are where the insight is. A single review that says "Delivery was fast, but support took three days to reply" is mixed overall, and the aspect array tells you which part to fix. Store the array in a JSON column and cast it on the model:

app/Models/Review.php PHP
protected function casts(): array
{
    return [
        'aspect_sentiments' => 'array',
    ];
}

// In the migration:
// $table->json('aspect_sentiments')->nullable();

Then save it from the job next to the overall result:

app/Jobs/AnalyzeReviewSentiment.php (update) PHP
$this->review->update([
    'sentiment' => $result['sentiment'],
    'sentiment_confidence' => $result['confidence'],
    'aspect_sentiments' => $result['aspect_sentiments'] ?? null,
]);

Choose aspects that match how customers talk about your business. A shop might use delivery, quality, price and support; a SaaS product might use onboarding, performance and billing. The API accepts up to 10 aspects per request, but fewer, well-chosen aspects give cleaner reports.

6. Query the results for a dashboard

Because sentiment is stored in an indexed column, reporting is plain Eloquent. Here are two queries that cover most dashboards:

Reporting queries PHP
// Share of each sentiment over the last 30 days
$breakdown = Review::where('created_at', '>=', now()->subDays(30))
    ->whereNotNull('sentiment')
    ->selectRaw('sentiment, COUNT(*) as total')
    ->groupBy('sentiment')
    ->pluck('total', 'sentiment');

// Negative reviews by week, to spot a spike after a release
$negativeByWeek = Review::where('sentiment', 'negative')
    ->where('created_at', '>=', now()->subWeeks(12))
    ->selectRaw('YEARWEEK(created_at, 1) as week, COUNT(*) as total')
    ->groupBy('week')
    ->orderBy('week')
    ->pluck('total', 'week');

For alerts, check the confidence value before notifying anyone. A negative result at 0.95 confidence deserves an immediate message to support; a negative result at 0.55 is better queued for a human to read first.

7. Back-fill existing reviews

If you already have thousands of reviews, create an Artisan command that dispatches the job for rows without a sentiment. Space the jobs out so you stay inside the rate limit (60 requests per minute per endpoint on a standard account):

app/Console/Commands/BackfillSentiment.php PHP
public function handle(): int
{
    $delay = 0;

    Review::whereNull('sentiment')->chunkById(200, function ($reviews) use (&$delay) {
        foreach ($reviews as $review) {
            AnalyzeReviewSentiment::dispatch($review)->delay(now()->addSeconds($delay));
            $delay += 1; // roughly 60 jobs per minute
        }
    });

    $this->info('Queued sentiment analysis for all unscored reviews.');

    return self::SUCCESS;
}

Run the command once, then let the observer handle new reviews as they arrive. Because the job releases itself and retries when the API is unavailable, an interrupted back-fill finishes on its own.

8. Test your integration with Http::fake

Never call the real API from your test suite. Laravel's Http::fake() replaces the HTTP client so your tests run offline and deterministically. Fake both a success and a failure, because the failure path is where integrations usually break:

tests/Feature/SentimentServiceTest.php PHP
use App\Services\SentimentService;
use Illuminate\Support\Facades\Http;

it('returns the sentiment result', function () {
    Http::fake([
        'rsflowhub.com/*' => Http::response([
            'success' => true,
            'data' => ['result' => ['sentiment' => 'negative', 'confidence' => 0.93]],
        ]),
    ]);

    $result = app(SentimentService::class)->analyze('Broke after two days.');

    expect($result['sentiment'])->toBe('negative');
});

it('returns null when the API fails', function () {
    Http::fake(['rsflowhub.com/*' => Http::response(['success' => false], 500)]);

    expect(app(SentimentService::class)->analyze('Anything'))->toBeNull();
});

9. Errors, limits and cost

  • Errors use the same envelope with success: false and an error.code such as validation_error (422), insufficient_credits (402) or rate_limit_exceeded (429).
  • Rate limits are per minute and depend on your plan. On a 429, wait for the retry delay; the retry() call above handles short spikes.
  • Cost: sentiment analysis is a utility endpoint, so it is covered by the free monthly request quota on every plan before any credits are used. Identical repeated texts are served from cache.

10. Common mistakes to avoid

  • Calling the API inside the request cycle. A slow AI call makes your page slow. Queue it.
  • Analysing very long text in one request. The limit is 5,000 characters; trim or split.
  • Treating confidence as optional. Low-confidence results should not trigger automated actions such as emails or refunds.
  • Counting neutral as satisfied. A neutral review is not a happy customer. Report neutral separately.
  • Ignoring failures. If the job silently gives up, your dashboard will under-report negative reviews. Log failures and release the job for a retry.
  • Hard-coding the key. Keep it in .env and read it through config(), so it works with config:cache and never lands in version control.

Get Your Free API Key

Sentiment analysis is included in the free monthly utility quota on every RSFlowHub plan, so you can run this tutorial without paying anything.

Create Free Account →

Key Takeaways

  • Laravel needs no extra packages: the Http facade is enough.
  • Read results from data.result; the envelope is the same for every endpoint.
  • Run AI calls in queued jobs, never in the request cycle.
  • Pass aspects to get per-topic sentiment such as delivery vs support.
  • Sentiment calls count toward the free monthly utility quota.
  • Store aspect_sentiments in a JSON column to see which part of an experience went wrong.
  • Test with Http::fake so your suite never calls the real API.

Frequently Asked Questions

No. Laravel includes the Http client, so a single POST request to the sentiment analysis endpoint is enough. There is no model to download or host.

Large language models handle sarcasm, mixed opinions and context much better than keyword lists. Check the confidence score and route low-confidence results to a human if they drive important decisions.

Yes. Pass an aspects array such as delivery, price and support, and the response includes a sentiment and confidence for each aspect.

Up to 5,000 characters per request. Split longer documents into paragraphs and analyse them separately.

Create an Artisan command that dispatches a queued job for every row without a sentiment, and delay each job by about one second to stay inside the 60 requests per minute default rate limit.

Use Http::fake() to return a canned response, so tests run offline and never spend quota. Fake both a successful response and an HTTP 500 to check that your service returns null and your job retries.

Every plan includes a free monthly quota of utility requests that covers sentiment analysis. After the quota, each call costs credits starting at 1 credit.

Reviews & Discussion (0)

0

Based on 0 ratings

5 ★
0%
4 ★
0%
3 ★
0%
2 ★
0%
1 ★
0%
No comments yet

Be the first to share your thoughts on this article!

Unified AI Access ⚡

One API Key. Infinite Possibilities.

Get access to OpenAI, Anthropic, Gemini, and more through a single, secure gateway. Start building in minutes with zero setup friction.