Calling Magento SOAP API from Perl with SOAP::LiteAug 02 2010 | 13:11:40 | 3 Comments

We had a legacy order fulfillment library written in Perl, so although accessing the API is extremely easy in PHP, when we migrated to Magento I had to figure out how to connect to the SOAP API from Perl.

Turns out the most popular CPAN module for doing SOAP in Perl (SOAP::Lite) is not particularly fun to use (see “State of the SOAP” at http://www.soaplite.com/ for a three-year-old call for volunteers to refactor).

Nevertheless, I did eventually emerge victorious. There may be a much easier way but after a lot of head-scratching and trial and error, this worked for me, YMMV.

The soapify routine was stolen from this page:
http://www.soaplite.com/2004/01/building_an_arr.html
Many thanks to soapify author Sandeep Satavlekar.

For passing the filter args to sales_order.list, I had to make the call in PHP first and then reverse-engineer the encoding of the nested array that actually got passed. There may be a way to get SOAP::Lite to encode nested arrays like this by default (or encode nested arrays in some other standard SOAPy way that Magento would also accept) but I chose not to waste any more time on this than I already had.

Enjoy!

UPDATE: Was getting sporadic “Access Denied” errors. Turns out you can fix this just by turning on cookies. Apparently this only affects you if Magento is using DB sessions. See new get_soap_session below for how to turn on cookies.

UPDATE TO THE UPDATE: Nah, that didn’t fix it. Still searching for a fix… runs fine in a session, fails about 80% of the time from cron.

The complete working example script is after the jump...


#!/usr/bin/perl

# replace these values with the soap account you set up in the admin
my $user = 'username';
my $pass = 'password';

# replace with your domain and path (we installed magento at /shop/)
my $soap_url = 'http://YOURDOMAIN/MAGENTO_PATH/index.php/api/index/index/';

my ($soap,$session_id) = &get_soap_session($user,$pass,$soap_url);

# simple example (one arg of type string)
my $oid = '1000001';
my $order = &get_order($soap,$session_id,$oid);

# gnarly example (argument is nested arrays)
my $orders = &get_recent_invoiced_orders($soap,$session_id);

foreach my $ord (@$orders) {
    my $due = $ord->{grand_total};
    my $paid_or_authorized = $ord->{total_invoiced};
    my $owed = $due - $paid_or_authorized;
    # ignore if not yet paid
    next if ($owed > .02);
    &fulfill_order($ord);
}

exit 0;

# 
# subroutines... should do this more OO but this is
# some simple, readable example code to help you avoid
# exploring the dark cave that I went into...
# 
sub get_order {
    my $soap = shift;
    my $session_id = shift;
    my $oid = shift;

    # Yes, we're really calling a method named "call" -- confusing, no?
    #
    # For simple calls with no args or just one or two string args, this
    # straightforward code works fine.
    #
    # The first argument is always the session ID that was returned from the initial
    # login call.  The second argument is the requested API resource.
    # Third is a list of any arguments to pass to the resource that's being invoked.
    my $resp = $soap->call('call' => ($session_id, 'sales_order.info', ($oid)));
    return $resp->result;
}


sub get_recent_invoiced_orders {
    my $soap = shift;
    my $session_id = shift;

    # limit query to orders with activity in past 60 days
    my $sixty_days_back = &sixty_days_back();

    # If you can believe it, the crazy array below is really just this:
    #  my $filter_a, $filter_b;
    #  $filter_a->{status}->{eq} = 'processing';
    #  $filter_b->{updated_at}->{gt} = $sixty_days_back;
    #  my $filters = [ $filter_a, $filter_b ];

    # look up correct namespace prefixes for soapenc and xsi
    my $serializer = $soap->serializer();
    my $soapenc = $serializer->encprefix || 'soapenc';
    my $xsi = $serializer->find_prefix('http://www.w3.org/2001/XMLSchema-instance') || 'xsi';

    # There might be some way to automatically generate the mess
    # below from the simpler data structure above, but this works for now.
    my $ugly_args = ['item',
                     [
                      ['item',
                       [['key','status'],
                        ['value',
                         ['item',
                          [['key','eq'],['value','processing']]
                         ],
                         {"$xsi:type"=>'apache:Map'}
                        ]
                       ]
                      ],
                      ['item',
                       [['key','updated_at'],
                        ['value',
                         ['item',
                          [['key','gt'],['value',$sixty_days_back]]
                         ],
                         {"$xsi:type"=>'apache:Map'}
                        ]
                       ]
                      ]
                     ],
                     {"$xsi:type"=>'apache:Map'}
        ];


    my $ugly = ['args',$ugly_args,{"$soapenc:arrayType"=>'apache:Map[1]',"$xsi:type","$soapenc:Array"}];


    my @soap_args;
    $soap_args[0] = ['sessionId',$session_id];
    $soap_args[1] = ['resourcePath','sales_order.list'];
    $soap_args[2] = $ugly;

    my @data;
    foreach my $arg (@soap_args) {
        push @data, &soapify_args($arg);
    }

    my $resp = $soap->call('call' => @data);
    return $resp->result;
}

####################################################################
# soapify_args
#
# Description:
# Given an arrayref (infinite depth), constructs a valid SOAP Object
#
# Parameters:
# $args: Arrayref
#
# Format of the arrayref:
# [xml_element_name, value, attributes]
# xml_element_name must be a scalar, value could be a scalar or another arrayref of the same format and
# attributes must be a hashref. xml_element_name and attributes are optional.
#
# Examples:
# 1. ["ElemName1", "ElemValue1", { "xmlns" => "http://blah/blah"}]
# 2. ["ElemName2", [
# ["NameAtLevel1", "ValueAtLevel1", {}],
# ], {"attrname" => "attrvalue"}]
# 3. [
# ["name", "value"]
# ]
#
# Return Value:
# SOAP object
############################################################
sub soapify_args {
    my ($args) = (@_);

    die "The argument must be an arrayref!" if (ref($args) !~ /ARRAY/ );

    my @namevaluearray = @$args;
    my $soapobject = new SOAP::Data;

    if (defined ($namevaluearray[0]) && ref($namevaluearray[0]) !~ /ARRAY/) {

#------------------name------------------------# 
        $soapobject->name($namevaluearray[0]);

#------------------value-----------------------#
        if (defined $namevaluearray[1] && !ref($namevaluearray[1])) {
            $soapobject->value($namevaluearray[1]);
        }
        elsif (ref($namevaluearray[1]) =~ /ARRAY/) {
            my $soapvalue = soapify_args($namevaluearray[1]);
            my @pass = (ref($soapvalue) =~ /ARRAY/) ? @$soapvalue : ($soapvalue);
            $soapobject->value(SOAP::Data->value(@pass));
        }

#-------------------attribute------------------#
        my $attr = $namevaluearray[2];
        if (ref($attr) =~ /HASH/) {
            $soapobject->attr($attr);
        }
    }
    elsif (ref($namevaluearray[0]) =~ /ARRAY/) {
        my @valuesarray;
        foreach my $element (@namevaluearray) {
            my $reftoelement = (ref($element) =~ /ARRAY/) ? $element : [$element];
            push(@valuesarray, soapify_args($reftoelement));
        }
        return [@valuesarray];
    }

    return $soapobject;
}

sub get_soap_session {
    use HTTP::Cookies;
    use SOAP::Lite;
    on_fault => sub { my($soap, $res) = @_;
        die ref $res ? $res->faultstring : $soap->transport->status, "n";
    };
    #optional, uncomment this to debug xml going back and forth
    ##    SOAP::Lite->import(trace =>debug);

    my $user = shift;
    my $pass = shift;
    my $soap_url = shift;

    # need to turn cookies on, might get "Access Denied" errors otherwise
    my $soap = SOAP::Lite->new();
    $soap->proxy($soap_url, cookie_jar => HTTP::Cookies->new(ignore_discard => 1));

    # had to add this namespace to enable passing nested arrays as valid data
    my $serializer = $soap->serializer();
    $serializer->register_ns( 'http://xml.apache.org/xml-soap', 'apache' );

    my $session_id = $soap->login($user,$pass)->result;
    return ($soap,$session_id);
}

sub fulfill_order {
    my $ord = shift;

    # whatever you need to do, do it here... we pass the order to our SAP system
    # via a custom web service.  Maybe you transmit somewhere via EDI?

    # placeholder -- dump raw order data
    use Data::Dumper;
    print Dumper($ord);

    return;
}

sub sixty_days_back {
    my $sixty_days_in_secs = 60 * 24 * 60 * 60;
    my ($sec,$min,$hr,$mday,$mon0,$yr1900) = localtime(time - $sixty_days_in_secs);
    my $year = $yr1900 + 1900;
    my $mon = $mon0 + 1;
    # eg 2010-08-01 for august 1 2010
    return sprintf("%04d-%02d-%02d",$year,$mon,$mday);
}

3 Responses to “Calling Magento SOAP API from Perl with SOAP::Lite”

  1. Glad to know that it was of some use to you. I’d be glad to know if you find any bugs in the sub and report those to me.

    Thanks for the credit and mentioning my name on this site. Wish you happy coding!

    Cheers!

    -Sandeep

  2. That looks indeed really ugly!

    if it works, fine!

    I’m quite novice, had been struggling with XML disasters and more and if I look at the code, it looks to me that something is missing:

    That whole array looks like a silly wrapper for somethiong that should look like

    status

    eq
    processing

    and so on

    shouldn’t there be some kind of convenience methode do that ?

  3. Just wanted to thank you for the script – I just used it to integrate magento orders into a legacy Perl app for warehouse management

    It saved me a lot of digging

    Cheers mate

Leave a Reply